Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# Changelog

All notable changes to `atlas` will be documented in this file.

## 2.1.0

### Added

- `admin_level` column on `states` table — integer indicating hierarchical depth (1 = top-level, 2 = subdivision). Allows filtering states by administrative level for cleaner dropdowns.
- `parent_id` column on `states` table — nullable self-referential foreign key enabling tree navigation between administrative levels (e.g. autonomous community → province).
- `State::parent()` and `State::children()` Eloquent relationships for hierarchical traversal.
- `State::topLevel()` and `State::adminLevel(int $level)` query scopes for convenient filtering.
- Ceuta and Melilla added to `cities.json` as city entries.

### Changed

- `states.json` enriched with `admin_level` and `parent_id` fields for all 5038 entries. Run `php artisan atlas:states` to populate the new data.
- States are now sorted by country, then by `admin_level`, then by name.
- `admin_level` assigned for all 95 countries with multiple administrative division types.
- `parent_id` populated for Spain (ES), France (FR), Italy (IT) and Belgium (BE).
- Ceuta and Melilla (Spain) reclassified from `admin_level: 1` to `admin_level: 2` to appear alongside provinces in address forms.

### Upgrade steps

1. Run `php artisan migrate` to add the new columns (defaults ensure existing data remains valid).
2. Run `php artisan atlas:states` to re-seed with hierarchical data.
3. Optionally run `php artisan atlas:cities` to seed the new Ceuta/Melilla city entries.
4. Use `State::where('country_id', $id)->topLevel()->get()` in dropdowns where you only want first-level divisions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ class MyClass

```

### State hierarchy

States support multi-level administrative divisions. Use `admin_level` to distinguish between primary regions (level 1) and subdivisions (level 2+):

```php
use Raiolanetworks\Atlas\Models\State;

// Get only top-level divisions (e.g., Autonomous Communities in Spain)
State::where('country_code', 'ES')->topLevel()->get();

// Get subdivisions of a specific state
$catalonia = State::where('state_code', 'CT')->first();
$catalonia->children; // Provinces: Barcelona, Girona, Lleida, Tarragona

// Navigate up the hierarchy
$barcelona = State::where('name', 'Barcelona')->first();
$barcelona->parent; // Cataluña
```

> **Note:** Parent-child relationships are currently populated for ES, FR, IT and BE. Other countries have `admin_level` set but `parent_id` is null.


## Upgrading

Expand Down
84 changes: 84 additions & 0 deletions database/migrations/0000_03_07_190512_add_state_hierarchy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* Adds admin_level and parent_id columns to the states table for
* hierarchical administrative divisions. Every operation is guarded
* for idempotency so this migration is safe on both fresh installs
* and upgrades.
*/
public function up(): void
{
if (! config()->boolean('atlas.entities.states')) {
return;
}

$statesTable = config()->string('atlas.states_tablename');

if (! Schema::hasTable($statesTable)) {
return;
}

if (! Schema::hasColumn($statesTable, 'admin_level')) {
Schema::table($statesTable, function (Blueprint $table): void {
$table->unsignedTinyInteger('admin_level')->default(1)->after('type');
});
}

if (! Schema::hasColumn($statesTable, 'parent_id')) {
Schema::table($statesTable, function (Blueprint $table) use ($statesTable): void {
$table->unsignedBigInteger('parent_id')->nullable()->after('admin_level');

$table->foreign('parent_id')
->references('id')
->on($statesTable)
->nullOnDelete();
});
}

$this->addMissingIndexes($statesTable);
}

/**
* Reverse the migrations.
*/
public function down(): void
{
// Not reversible: column additions and indexes are intentionally kept.
// The original CREATE TABLE migration handles full table drops.
}

/**
* Add indexes for the new columns if they don't already exist.
*/
private function addMissingIndexes(string $statesTable): void
{
if (! Schema::hasIndex($statesTable, "{$statesTable}_admin_level_index")) {
Schema::table($statesTable, function (Blueprint $table): void {
$table->index('admin_level');
});
}

if (! Schema::hasIndex($statesTable, "{$statesTable}_parent_id_index")
&& ! Schema::hasIndex($statesTable, "{$statesTable}_parent_id_foreign")) {
Schema::table($statesTable, function (Blueprint $table): void {
$table->index('parent_id');
});
}

if (! Schema::hasIndex($statesTable, "{$statesTable}_country_id_admin_level_index")) {
Schema::table($statesTable, function (Blueprint $table): void {
$table->index(['country_id', 'admin_level']);
});
}
}
};
26 changes: 26 additions & 0 deletions resources/json/cities.json
Original file line number Diff line number Diff line change
Expand Up @@ -1963310,5 +1963310,31 @@
"latitude": "-20.30345000",
"longitude": "30.07514000",
"wikiDataId": "Q24235929"
},
{
"id": 154475,
"name": "Ceuta",
"state_id": 5223,
"state_code": "CE",
"state_name": "Ceuta",
"country_id": 207,
"country_code": "ES",
"country_name": "Spain",
"latitude": "35.88900000",
"longitude": "-5.31870000",
"wikiDataId": "Q5765"
},
{
"id": 154476,
"name": "Melilla",
"state_id": 5224,
"state_code": "ML",
"state_name": "Melilla",
"country_id": 207,
"country_code": "ES",
"country_name": "Spain",
"latitude": "35.29370000",
"longitude": "-2.93830000",
"wikiDataId": "Q5831"
}
]
Loading