plin-code / laravel-istat-geography
Laravel package for importing and managing Italian geography data from ISTAT
Package info
github.com/plin-code/laravel-istat-geography
pkg:composer/plin-code/laravel-istat-geography
Fund package maintenance!
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.0
- illuminate/contracts: ^12.0 || ^13.0
- league/csv: ^9.24
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.0 || ^4.0
- pestphp/pest-plugin-arch: ^3.0 || ^4.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel package for importing and managing Italian geographical data from ISTAT.
Features
- ๐ฎ๐น Import Italian regions, provinces, and municipalities from ISTAT
- ๐ฎ Import Italian postal codes (CAP) with support for multi-CAP municipalities
- ๐ Optional latitude and longitude for every municipality
- ๐ Incremental updates: add new records, update changes, soft-delete removed ones
- ๐ Daily CSV caching to avoid unnecessary requests
- ๐ Eloquent models with hierarchical relationships
- โก Artisan commands for easy data import and synchronization
- ๐ง Fully configurable via configuration file
- ๐ UUID primary keys and soft deletes support
- ๐งช Comprehensive test suite with mocked HTTP requests
Requirements
- PHP 8.3+
- Laravel 11.0+ or 12.0+
- league/csv 9.0+
- guzzlehttp/guzzle 7.0+
Installation
composer require plin-code/laravel-istat-geography
Quick Start
- Install the package:
composer require plin-code/laravel-istat-geography
- Publish the configuration:
php artisan vendor:publish --provider="PlinCode\IstatGeography\IstatGeographyServiceProvider"
- Run migrations:
php artisan migrate
- Import the data:
php artisan geography:import
That's it! You now have all Italian geographical data in your database.
Commands
geography:import
Performs a full import of all geographical data from ISTAT. Use this for the initial data load.
php artisan geography:import
Options
| Option | Description |
|---|---|
--cap |
Also import postal codes (CAP) after ISTAT data |
--cap-only |
Import only postal codes, skip ISTAT data (requires existing municipalities) |
--cap-file=<path> |
Use a local JSON file for CAP data instead of downloading |
--coordinates |
Also import municipality coordinates (latitude and longitude) after ISTAT data |
--coordinates-only |
Import only coordinates, skip ISTAT data (requires existing municipalities) |
--coordinates-file=<path> |
Use a local JSON file (plain or gzipped) for coordinates instead of downloading |
Examples
# Import ISTAT data only php artisan geography:import # Import ISTAT data + CAP (using local file - recommended) php artisan geography:import --cap --cap-file=cap-dataset.json # Update only CAP on existing municipalities php artisan geography:import --cap-only --cap-file=cap-dataset.json # Import ISTAT data + coordinates php artisan geography:import --coordinates # Update only coordinates on existing municipalities php artisan geography:import --coordinates-only
Note: The remote GeoJSON with geometries is ~464MB. Using
--cap-filewith a preprocessed JSON file (~3MB) is recommended for better performance.
geography:download-cap
Downloads CAP GeoJSON data and saves it locally for offline import. Useful when you want to download once and import multiple times.
# Download from default URL (config/env) php artisan geography:download-cap # Download from custom URL php artisan geography:download-cap --url=https://example.com/cap.json # Specify output path php artisan geography:download-cap --output=storage/app/my-cap.json
After downloading, import with:
php artisan geography:import --cap --cap-file=storage/app/cap-dataset.json
geography:download-coordinates
Downloads the coordinates dataset and saves it locally (decompressed) for offline import.
# Download from default URL (config/env) php artisan geography:download-coordinates # Download from custom URL php artisan geography:download-coordinates --url=https://example.com/coordinates.json.gz # Specify output path php artisan geography:download-coordinates --output=storage/app/my-coordinates.json
After downloading, import with:
php artisan geography:import --coordinates-only --coordinates-file=storage/app/coordinates-dataset.json
geography:update
Incrementally synchronizes your database with the latest ISTAT data. It compares the current ISTAT CSV against your existing records and applies only the differences: new records are added, changed records are updated, and records no longer present in ISTAT are soft-deleted.
php artisan geography:update
Options
| Option | Description |
|---|---|
--dry-run |
Simulate the update without making any database changes. Shows what would be added, modified, or deleted. |
--force |
Continue execution even if non-critical errors occur (errors are logged as warnings). |
Verbosity Levels
| Flag | Output |
|---|---|
| (none) | Final summary only (e.g. 3 added, 1 modified, 0 deleted) |
-v |
Download progress, list of new/modified/suppressed records, progress bar |
-vv |
Field-level change details (e.g. name: Old Name โ New Name) |
-vvv |
Debug output with timing information for each operation |
Examples
# Preview changes without applying them php artisan geography:update --dry-run # Run with verbose output php artisan geography:update -v # Run with full debug output php artisan geography:update -vvv # Force continue on non-critical errors php artisan geography:update --force
All database operations are wrapped in a transaction. If any error occurs (and --force is not set), all changes are automatically rolled back.
Configuration
Publish the configuration file:
php artisan vendor:publish --provider="PlinCode\IstatGeography\IstatGeographyServiceProvider"
The config/istat-geography.php file allows you to customize:
- Database connection: Choose which database connection the package tables should use (defaults to the main connection)
- Table names: Customize the database table names
- Model classes: Use your own model classes by extending the base ones
- CSV URL: Change the ISTAT data source URL (also via
ISTAT_CSV_URLenv) - CAP GeoJSON URL: Change the CAP data source URL (also via
CAP_GEOJSON_URLenv) - Coordinates: Import coordinates on every
geography:import(also viaISTAT_IMPORT_COORDINATESenv) and change the dataset URL (also viaISTAT_COORDINATES_URLenv) - Temporary file name: Customize the cache file name
Database Connection
By default the package uses your application's default database connection. To store the geographical tables on a separate connection, set the connection key in config/istat-geography.php or the ISTAT_DB_CONNECTION environment variable:
ISTAT_DB_CONNECTION=geography
Note
The connection config key is additive and fully backward compatible. If you published the config file before this option existed, the package falls back to your default database connection (config('database.default')), so no action is required on upgrade. To opt into a custom connection, either republish the config file or set the ISTAT_DB_CONNECTION environment variable.
Municipality Coordinates
Coordinates are optional. The extend_municipalities_with_coordinates migration adds two nullable columns, latitude and longitude, to the municipalities table, and they stay empty until you import them.
Import them once with --coordinates, or enable the import on every geography:import run:
ISTAT_IMPORT_COORDINATES=true
Each municipality gets one representative point of its administrative area: the centroid of the municipal boundary when it falls inside the municipality, otherwise a point guaranteed to be inside it. The point is not the town hall or the main inhabited centre, so for large or irregular municipalities it can be a few kilometres away from the town centre (and sometimes in open countryside). It is meant for maps, sorting by distance and proximity searches, not for navigation.
geography:update never overwrites coordinates, because they are not ISTAT fields. When ISTAT adds a new municipality, it gets coordinates the next time you run the coordinates import with an updated dataset.
$municipality = Municipality::where('istat_code', '058091')->first(); $municipality->latitude; // 41.8853588 $municipality->longitude; // 12.4607809
Example Configuration
return [ 'connection' => env('ISTAT_DB_CONNECTION', env('DB_CONNECTION')), 'tables' => [ 'regions' => 'my_regions', 'provinces' => 'my_provinces', 'municipalities' => 'my_municipalities', ], 'models' => [ 'region' => \App\Models\Region::class, 'province' => \App\Models\Province::class, 'municipality' => \App\Models\Municipality::class, ], 'import' => [ 'csv_url' => 'https://custom-url.com/data.csv', 'temp_filename' => 'my_istat_data.csv', ], ];
Models
The package provides three Eloquent models:
Region
use PlinCode\IstatGeography\Models\Geography\Region; $region = Region::where('name', 'Piemonte')->first(); $provinces = $region->provinces;
Province
use PlinCode\IstatGeography\Models\Geography\Province; $province = Province::where('code', 'TO')->first(); $municipalities = $province->municipalities; $region = $province->region;
Municipality
use PlinCode\IstatGeography\Models\Geography\Municipality; $municipality = Municipality::where('name', 'Torino')->first(); $province = $municipality->province;
ISTAT Fields
Each model exposes a static istatFields() method that returns the list of fields managed by ISTAT data. These are the fields that the geography:update command is allowed to overwrite. Any additional fields you add to your extended models will not be touched during updates.
Region::istatFields(); // ['name', 'istat_code'] Province::istatFields(); // ['name', 'code', 'istat_code', 'region_id'] Municipality::istatFields(); // ['name', 'istat_code', 'province_id', 'bel_code'] Municipality::capFields(); // ['postal_code', 'postal_codes'] Municipality::coordinateFields(); // ['latitude', 'longitude']
Extending Models
If you want to use the package models in your main project, you can extend them:
// app/Models/Region.php namespace App\Models; use PlinCode\IstatGeography\Models\Geography\Region as BaseRegion; class Region extends BaseRegion { // Add your project-specific logic here public function customMethod() { return $this->provinces()->count(); } }
// app/Models/Province.php namespace App\Models; use PlinCode\IstatGeography\Models\Geography\Province as BaseProvince; class Province extends BaseProvince { // Add your project-specific logic here }
// app/Models/Municipality.php namespace App\Models; use PlinCode\IstatGeography\Models\Geography\Municipality as BaseMunicipality; class Municipality extends BaseMunicipality { // Add your project-specific logic here }
Remember to update the models section in the configuration file to point to your custom classes.
Database Structure
Regions
id(UUID, primary key)name(string)istat_code(string, unique)created_at,updated_at,deleted_at
Provinces
id(UUID, primary key)region_id(UUID, foreign key)name(string)code(string, unique)istat_code(string, unique)created_at,updated_at,deleted_at
Municipalities
id(UUID, primary key)province_id(UUID, foreign key)name(string)istat_code(string, unique)bel_code(string, nullable) - Cadastral/Belfiore code for CAP matchingpostal_code(string, nullable) - Primary postal code (CAP)postal_codes(string, nullable) - Range of postal codes for large municipalities (e.g., "00118-00199")latitude(decimal 10,7, nullable): latitude of the representative point (WGS 84)longitude(decimal 10,7, nullable): longitude of the representative point (WGS 84)created_at,updated_at,deleted_at
Relationships
RegionโhasManyโProvinceProvinceโbelongsToโRegionProvinceโhasManyโMunicipalityMunicipalityโbelongsToโProvince
Replacing Existing Command
If you already have a geography:import command in your project, you can replace it with the package's command:
// In app/Console/Kernel.php or in your existing command Artisan::command('geography:import', function () { $this->info('Starting geographical data import...'); try { $count = \PlinCode\IstatGeography\Facades\IstatGeography::import(); $this->info("Import completed successfully! Imported {$count} municipalities."); } catch (\Exception $e) { $this->error('Error during import: ' . $e->getMessage()); } })->purpose('Import regions, provinces and municipalities from ISTAT');
Testing
Run the test suite:
composer test
The package includes:
- โ Unit tests for models and relationships
- โ Feature tests for the import service
- โ Feature tests for the update command and services
- โ Mocked HTTP requests (no external dependencies)
- โ PHPStan static analysis
- โ Pest PHP testing framework
Test Coverage
- Models and their relationships
- Import service with CSV processing
- Compare service for detecting changes
- Update service for applying changes
- Artisan command functionality (import and update)
- Configuration handling
Data Sources
ISTAT Data
Geographic data (regions, provinces, municipalities) is sourced from ISTAT (Italian National Institute of Statistics), the official Italian government statistics agency.
Postal Codes (CAP) and Boundaries
Postal code data (data-properties-v1) and the full boundaries GeoJSON (data-geo-v1) are sourced from Zornade Data Downloads. They are derived from OpenStreetMap addresses, so they are licensed under the Open Database License (ODbL). If you use or redistribute them, keep this attribution and share derived databases under the same license:
ยฉ OpenStreetMap contributors (ODbL). Data processed by Zornade (https://zornade.com).
Municipality Coordinates
Coordinates are derived from the ISTAT municipal boundaries (Confini Amministrativi) published by Zornade Data Downloads, licensed under CC BY 4.0. If you show these coordinates in your product, keep this attribution visible:
Source: ISTAT, administrative boundaries (CC BY 4.0). Data processed by Zornade (https://zornade.com).
The points were computed by this package from the boundary polygons (centroid in EPSG:3035, or a point on surface when the centroid falls outside the polygon), so they are an adaptation of the original data. The dataset file includes the same attribution, license and generation date in its meta section.
A huge thanks to Zornade for their incredible work in making Italian public data freely available. Their dedication to open data helps developers build better applications for Italian users.
Regenerating the Coordinates Dataset
Maintainers can rebuild the dataset with scripts/build-coordinates-dataset.php (PHP with pdo_sqlite, plus Docker for the official GDAL image). It downloads the boundaries and the current ISTAT list, computes one point per municipality and writes build/municipality_coordinates_dataset.json.gz:
php scripts/build-coordinates-dataset.php
# Optional sanity check with the Zornade reverse geocoding API
ZORNADE_TOKEN=your-token php scripts/build-coordinates-dataset.php --spot-check
Municipalities created by a merger after the boundaries were published are computed on the union of their predecessors, listed in MERGED_MUNICIPALITIES inside the script. The script stops with an error when a municipality has no polygon and no merger entry.
Contributing
- Fork the project
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
The MIT License (MIT). Please see License File for more information.
