codecruz / spain-postal-codes
Spanish postal codes, municipalities and provinces from official INE data. Offline lookup, zero dependencies.
Requires
- php: ^8.1
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-08 00:29:13 UTC
README
This library looks up and searches Spanish postal codes, municipalities and provinces without network access. It is built from official INE data.
- Coverage: 10,791 postal codes and 8,132 municipalities, with their INE codes.
- Light: no dependencies, PHP 8.1+, and it works with any framework (Laravel, Symfony, WordPress…) or none.
- Forgiving input: restores lost leading zeros (
1450→01450). Name searches ignore accents and case. - Typed results: immutable value objects that can be serialized to JSON.
This is the PHP version of spain-postal-codes, which also offers the CSV dataset, a static JSON API and an npm package with the same API.
Install
composer require codecruz/spain-postal-codes
Usage
use Codecruz\SpainPostalCodes\PostalCodes; $municipios = PostalCodes::lookup('41700'); $municipios[0]->nombre; // "Dos Hermanas" $municipios[0]->provincia; // "Sevilla" $municipios[0]->id; // "41038" (INE code) // One postal code can serve several municipalities, even in different provinces. array_map(fn ($m) => $m->nombre, PostalCodes::lookup('01427')); // ["Valdegovía/Gaubea", "Berberana", "Jurisdicción de San Zadornil"] PostalCodes::isValidPostalCode(8001); // true ("08001") PostalCodes::getProvincia('28001')?->nombre; // "Madrid" PostalCodes::getProvincia('vizcaya')?->nombre; // "Bizkaia" PostalCodes::getPostalCodes('28079'); // ["28001", "28002", ...] PostalCodes::searchMunicipios('alcala', provinciaId: '28', limit: 5); // [Municipio("Alcalá de Henares"), Municipio("Valverde de Alcalá")]
Validating a form
$code = PostalCodes::normalizePostalCode($request->get('postal_code')); $municipios = $code === null ? [] : PostalCodes::lookup($code); if ($municipios === []) { // unknown postal code }
Returning JSON
Municipio and Provincia implement JsonSerializable:
echo json_encode(PostalCodes::lookup('28001'), JSON_UNESCAPED_UNICODE); // [{"id":"28079","nombre":"Madrid","provinciaId":"28","provincia":"Madrid","codigosPostales":["28001",...]}]
API
All methods are static on Codecruz\SpainPostalCodes\PostalCodes.
| Method | Returns |
|---|---|
lookup(string|int $postalCode) |
list<Municipio>: municipalities served by the code (empty if unknown) |
isValidPostalCode(string|int $postalCode) |
bool: whether the code exists |
normalizePostalCode(string|int $postalCode) |
?string: 5-digit form, or null if malformed |
getMunicipio(string $id) |
?Municipio: by 5-digit INE code |
getPostalCodes(string $municipioId) |
list<string>: postal codes of a municipality |
getProvincia(string|int $value) |
?Provincia: by code ('28'), postal code ('28001') or name ('Vizcaya', 'alacant') |
getProvincias() |
list<Provincia>: the 52 provinces, sorted by code |
searchMunicipios(string $query, ?string $provinciaId = null, int $limit = 10) |
list<Municipio>: name search; prefix matches first |
metadata() |
array{period, referenceDate, source, license, attribution} |
final class Municipio { public readonly string $id; // "28079" public readonly string $nombre; // "Madrid" public readonly string $provinciaId; // "28" public readonly string $provincia; // "Madrid" public readonly array $codigosPostales; // list<string>, sorted } final class Provincia { public readonly string $id; // "28" public readonly string $nombre; // "Madrid" }
Performance: the data (~390 KB of PHP arrays) is loaded on the first call and kept for the rest of the request. Without opcache, loading takes about 15 ms and uses about 11 MB of memory. Once loaded, a lookup takes well under a microsecond.
Data
The postal codes come from the INE Callejero del Censo Electoral and the names from the INE municipality list. See how the dataset is built. Postal codes that don't belong to a street address, such as PO boxes, are not included.
New data is released as a minor version after each INE publication (January and July). Check PostalCodes::metadata()['period'] to see which period you have installed.
Development
composer install composer test # PHPUnit composer analyse # PHPStan, level max composer build-data # regenerate resources/data.php from the published dataset
resources/data.php is generated: don't edit it by hand. Data errors should be reported in the dataset repository.
Releasing
Packagist reads tags straight from this repository, so there is nothing to upload and no token to manage. A new tag shows up on Packagist within seconds.
Versioning
The library follows Semantic Versioning:
| Change | Version bump |
|---|---|
| Bug fixes | patch |
| New INE data | minor |
| New methods or options | minor |
| Removed or renamed methods, changed return types | major |
Updating the data
- On the 5th of each month,
update-data.ymlchecks the published dataset and opens a pull request titleddata: update to INE period <period>if it changed. You can also start it by hand from the Actions tab (Update data → Run workflow). You can also runcomposer build-datalocally. - Review the diff of
resources/data.phpand merge it once CI is green. - Release a new minor version (below).
Publishing a version
-
Add the version to
CHANGELOG.md, with the INE period for data updates. Open a pull request and merge it intomain. -
Create a GitHub release on
mainwhose tag isvplus the version:gh release create v1.1.0 --target main --title "v1.1.0" --notes "Data: INE callejero 2027-01"
-
Check that the version is listed on Packagist and that it installs:
composer require codecruz/spain-postal-codes:^1.1
Don't move or delete a tag once it is published: Composer users may already have it locked. To fix a bad release, publish a new patch version.