plin-code/laravel-istat-geography
Laravel package for Italian ISTAT geography data, providing regions, provinces, and municipalities (comuni) ready to use in your app. Simple Eloquent integration for addresses, filters, and location-based features.
This package gives you Italian geographical data (regions, provinces, municipalities/CAP boundaries) as Eloquent models and migrations, so you don't hand-roll your own regions/provinces tables.
Install and migrate
composer require plin-code/laravel-istat-geography
php artisan vendor:publish --tag="istat-geography-config" # inspect config before migrating
php artisan migrate
Laravel 12 or 13 required.
Decide your table and model names first. As of v1.5.0 the tables config genuinely renames tables: the base migration creates the configured names and the package models use them. Set them before running php artisan migrate — changing the keys later does not rename anything that already exists. Likewise, if you want your own model classes (with extra scopes, casts, or traits), configure them under models.
Load the data. Use the geography:* artisan commands: geography:download-cap fetches CAP boundary data (default output storage/app/cap-boundaries.geojson — check --help), and the import command consumes it. Run php artisan geography:download-cap then the import, or wire both into a seeder/deploy script.
First useful use case: an address form. A region select, a province select filtered by region via $region->provinces(), and a municipality select filtered by province — all backed by real ISTAT data instead of a hardcoded array.
If your editor/agent supports Laravel Boost, the package now ships resources/boost/guidelines/core.blade.php, which teaches the agent about the models, tables, relations, geography:* commands and config keys. That's the fastest way to stop an agent from inventing its own regions table. laravel/boost is suggested only — nothing breaks without it.
Relations over manual queries. Use provinces(), region(), municipalities(), province(). Since v1.5.0 these resolve through the classes configured under models, with the same foreign keys as before — so swapping in your own model classes does not break the relation graph.
Custom models via config, not forks. Point models at your own classes and add scopes/casts there. Remember a $table property set on your own model still takes precedence over the config — useful for one-off tables, a footgun if you're inconsistent.
Eager-load for selects. Building a nested region → province → municipality form is the classic N+1 trap:
$regions = Region::with('provinces.municipalities')->get();
For large datasets, prefer a lazy endpoint that returns provinces for a chosen region.
Seeding and CI. Keep data loading in a dedicated seeder or deploy step, not in the base migration. The geography dataset is large; you don't want it re-imported on every migrate:fresh in tests — use a small fixture or RefreshDatabase with a subset.
Multi-tenant / multi-instance table names. The tables config makes it viable to run the package against differently named tables per connection, but only if the config is resolved before migration runs. For per-tenant setups, consider a separate connection + config set rather than renaming after the fact.
CAP boundaries. geography:download-cap writes GeoJSON to storage/app/cap-boundaries.geojson by default (the --help output and README now agree on this). Store the file outside the repo, and re-download on deploy or cache it in your artifact pipeline rather than committing large GeoJSON.
tables support is read at migration and model-resolution time, not a migration of existing data. Treat table names as a pre-migration decision. If you already migrated with defaults, write your own rename migration or re-run migrate:fresh before you have real data.$table on your model wins over config. Great escape hatch, easy to forget during a rename — audit custom models when you change the tables keys.PlinCode\...\Province in a closure or instanceof check), configure models accordingly or loosen the type hints. Foreign keys are unchanged, so queries still work.Http facade, so Guzzle comes from your app (7 on Laravel 12; 7 or 8 on Laravel 13). If you had a transitive reliance on the package pinning Guzzle, verify your own constraint — especially before jumping to Laravel 13.config('istat-geography.models') and clear config cache (php artisan config:clear) after edits. Config-cached deploys are the usual culprit.models and tables config, your own model classes with $table and custom relations, and the Boost guidelines file as a reference for what the package expects an agent to know.How can I help you explore Laravel packages today?