Search by

codecruz / spain-postal-codes

codecruz

Spanish postal codes, municipalities and provinces from official INE data. Offline lookup, zero dependencies.

v1.0.1 2026-11-08 00:23 UTC

This package is auto-updated.

Last update: 2026-11-08 00:29:13 UTC


README

CI Packagist PHP PHPStan

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

  1. On the 5th of each month, update-data.yml checks the published dataset and opens a pull request titled data: 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 run composer build-data locally.
  2. Review the diff of resources/data.php and merge it once CI is green.
  3. Release a new minor version (below).

Publishing a version

  1. Add the version to CHANGELOG.md, with the INE period for data updates. Open a pull request and merge it into main.

  2. Create a GitHub release on main whose tag is v plus the version:

    gh release create v1.1.0 --target main --title "v1.1.0" --notes "Data: INE callejero 2027-01"
  3. 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.

License

  • Code: MIT.
  • Data: derived from INE data, under CC BY 4.0. Credit the source: "Fuente: Instituto Nacional de Estadística (INE)".