Search by

stanislas-poisson / french-postal-code

zairakai

The regions, departments, communes and postal codes of France, with one GPS point per postal code: the data, the models and a loader for Laravel and Symfony.

4.0.0 2026-10-02 22:14 UTC

This package is auto-updated.

Last update: 2026-10-02 22:18:12 UTC


README

French Postal Code

The regions, departments, communes and postal codes of France, with one GPS point per postal code, in your own database.

Packagist CI License: MIT PHP 8.2+

A Composer package that gives an application the French administrative areas and postal codes as data, models with their relations and a command that loads them into its database. It is built to work with Laravel (Eloquent) and Symfony (Doctrine), and its core needs no framework.

The data comes from the French-Postal-Code project, which builds it from the official open sources (INSEE, La Poste, the Base Adresse Nationale) and publishes it on data.gouv.fr.

Why a package

An application that stores addresses wants to point each one to a stable postal entry, such as "37200 Tours", with a foreign key. This package keeps the original identifiers of the dataset when it loads the rows, and never deletes a row: a closed row has a valid_to date, and a replaced city points to its successor. The foreign keys of your application therefore stay valid from one version to the next.

What the data holds

Table Content
regions The regions.
departments The departments and the overseas collectivities.
communes The communes and the municipal arrondissements, closed ones included.
cities One row per commune and per postal code, with its GPS point. The table to reference by a foreign key.
commune_successions For an old INSEE code, the code that follows it, the kind of change and its date.

Every table has valid_from and valid_to columns. The files are in data/, described by a manifest.json and by a Table Schema per file.

Reading the data without a framework

use StanislasPoisson\FrenchPostalCode\Core\Dataset;

$dataset = new Dataset();

$dataset->count('cities');                  // 35510
$dataset->columns('cities');                // ['id', 'commune_id', 'postal_code', ...]

foreach ($dataset->chunks('cities', 1000) as $rows) {
    // $rows is a list of arrays indexed by column name, an empty value is null
}

The reader streams the files and checks them against the manifest: a truncated or altered file is an error, never a silent partial load.

Laravel

composer require stanislas-poisson/french-postal-code
php artisan migrate
php artisan french-postal-code:load

migrate creates the tables, french-postal-code:load fills them. The command can be run again at any time: it adds the new rows, updates the others and deletes nothing, so it is also how you take a new version of the data.

The models are in StanislasPoisson\FrenchPostalCode\Laravel\Models: Region, Department, Commune, City and CommuneSuccession, with their relations.

use StanislasPoisson\FrenchPostalCode\Laravel\Models\City;

$city = City::query()->where('postal_code', '37200')->firstOrFail();

$city->commune->name;                        // Tours
$city->commune->department->region->name;    // Centre-Val de Loire
City::query()->current()->count();           // only the rows that are valid today

An application points to a postal entry with a foreign key to french_cities:

// In a migration of your application
$table->foreignId('city_id')->constrained('french_cities');

// In your model
public function city(): BelongsTo
{
    return $this->belongsTo(City::class);
}

Configuration

Many applications already own a cities or a regions table, so the tables of the package are prefixed. Both settings can be set in the environment, or in the configuration file published with php artisan vendor:publish --tag=french-postal-code-config:

Setting Environment variable Default
table_prefix FRENCH_POSTAL_CODE_TABLE_PREFIX french_ (an empty value gives regions, cities...)
connection FRENCH_POSTAL_CODE_CONNECTION the default connection

Set them before running php artisan migrate.

Compatibility

Supported
PHP 8.2, 8.3, 8.4
Laravel 11 and 12 (PHP 8.2 or more), 13 (PHP 8.3 or more)
Databases Those Laravel supports, with the foreign keys enforced

Laravel 11 no longer receives security fixes, so Composer refuses to install it unless you accept its advisories; the adapter is tested on it nonetheless.

Symfony

composer require stanislas-poisson/french-postal-code

Register the bundle in config/bundles.php:

StanislasPoisson\FrenchPostalCode\Symfony\FrenchPostalCodeBundle::class => ['all' => true],

Create the tables with your usual Doctrine tooling, then load the data:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
php bin/console french-postal-code:load

The entities are mapped for you, so the migration your application generates contains the five tables, with their foreign keys. french-postal-code:load can be run again at any time: it adds the new rows, updates the others and deletes nothing, so it is also how you take a new version of the data.

The entities are in StanislasPoisson\FrenchPostalCode\Symfony\Entity: Region, Department, Commune, City and CommuneSuccession, with their relations and read-only accessors. The repositories add current(), a query builder on the rows that are valid today.

use StanislasPoisson\FrenchPostalCode\Symfony\Entity\City;

$city = $entityManager->getRepository(City::class)->findOneBy(['postalCode' => '37200']);

$city->getCommune()->getName();                                // Tours
$city->getCommune()->getDepartment()?->getRegion()?->getName(); // Centre-Val de Loire

An application points to a postal entry with a relation to City:

#[ORM\ManyToOne(targetEntity: City::class)]
#[ORM\JoinColumn(nullable: false)]
private City $city;

Configuration

# config/packages/french_postal_code.yaml
french_postal_code:
    table_prefix: french_   # an empty value gives "regions", "cities"...

Many applications already own a cities or a regions table, so the tables of the package are prefixed. Set it before generating the migration. The entities live in the default entity manager, on its connection.

Compatibility

Supported
PHP 8.2, 8.3, 8.4
Symfony 6.4 and 7.4 (PHP 8.2 or more), 8.0 (PHP 8.4)
Doctrine ORM 3 with DBAL 4, and DoctrineBundle 2 (Symfony 6.4 and 7) or 3 (Symfony 8)
Databases MySQL, MariaDB, PostgreSQL and SQLite

Below PHP 8.4, Doctrine ORM needs symfony/var-exporter to load a relation on demand; a Symfony application with Doctrine has it already.

Keeping a reference up to date

A row is never deleted. When a commune merges or a postal code changes, the old City is closed (valid_to is set) and points to the one that replaces it, so your foreign key keeps working and always leads to a row that exists. After loading a new version of the data, you can move the references that lead to a closed city.

Laravel:

// In your model: the closed city knows its replacement
$address->city->replacedBy;   // City or null

// Move every address that leads to a closed city to its replacement
Address::query()
    ->whereIn('city_id', City::query()->whereNotNull('replaced_by_city_id')->select('id'))
    ->update(['city_id' => City::query()->select('replaced_by_city_id')->whereColumn('french_cities.id', 'addresses.city_id')]);

Symfony:

$address->getCity()->getReplacedBy();   // City or null

// The same move, in SQL: adapt the names of your table and of the tables of the package (prefix included)
$entityManager->getConnection()->executeStatement(
    'UPDATE addresses SET city_id = (SELECT c.replaced_by_city_id FROM french_cities c WHERE c.id = addresses.city_id)
     WHERE city_id IN (SELECT id FROM french_cities WHERE replaced_by_city_id IS NOT NULL)'
);

A replacement can itself be replaced later: run the update again until it changes nothing, or follow replacedBy in a loop. For the old INSEE codes of a commune, commune_successions gives the commune or communes that replace it, with the date and the kind of change.

Updating the data

The files of data/ are never edited by hand. They are the package archive of a release of the builder:

make data                  # the latest release
make data VERSION=4.0.0    # a given release

The archive is checked against the SHA256SUMS file of the release before anything is replaced.

Versions

The version of the package follows SemVer. A new dataset without change of layout is a minor release, a change of the layout of the files is a major release, and a fix is a patch. The first release is 4.0.0, to follow the dataset it holds.

Development

make install
make test        # PHPUnit: the reader and the loader (see CONTRIBUTING.md for the adapters)
make quality     # Pint and PHPStan at the maximum level

See CONTRIBUTING.md, SECURITY.md and the code of conduct.

Licence

MIT for the code. The data stays subject to the licences of its sources, see the builder.