postal-code-formats
v0.1.0
Published
Postal code formats, validation regexes and 9,000 real sample codes for 31 countries. Every regex verified against the samples in CI. Zero dependencies.
Maintainers
Readme
postal-code-formats
Postal code formats, validation regexes and 9,000 real sample codes for 31 countries. Every regex is checked against every sample code in CI. Zero dependencies, no network calls.
Why another postal code list
Most of the postal code regexes circulating today trace back to one of two places: a
Wikipedia table scraped years ago, or Google's libaddressinput metadata, which is
still published but no longer maintained as a standalone dataset. Both are plausible.
Neither comes with anything that would tell you if a pattern had quietly gone wrong.
This dataset takes one position: a regex that has never been run against a real postal code is a guess. So every pattern here is tested against real codes for that country — 9,000 of them — and the test runs in CI on every push. If a pattern and the reality disagree, the build goes red rather than the data going quietly stale.
It also answers two questions the usual tables skip:
- Which countries have no postal code at all? Hong Kong is in this dataset with
has_postal_code: false, because a form that requires a postal code cannot be completed honestly there. Treating "no code" as "missing data" is a bug, not an edge case. - Is the sample a whole code or half of one? Canada, the UK, Ireland and the
Netherlands publish their codes in two halves, and open data usually carries only the
geographic half. Entries say which, in
sample_kind, instead of pretending a Forward Sortation Area is a full postal code.
Install
npm install postal-code-formatsOr take the data directly — data/postal-formats.json,
data/postal-formats.csv, and one file of real codes per
country under data/samples/. The data is plain and has no runtime;
the npm package is a convenience, not a dependency you are stuck with.
Use
import { validate, mask, localName, get } from "postal-code-formats";
validate("1012 AB", "NL"); // true
validate("1012ab", "NL"); // true — separator and case are normalized
validate("7030", "US"); // false — the leading-zero trap: 07030 is not 7030
validate("D4A 1B1", "CA"); // false — Canada Post never issues D, F, I, O, Q or U
mask("DE"); // "NNNNN" — N is a digit, A is a letter
localName("BR"); // "CEP" — label the field what the user calls it
localName("IE"); // "Eircode"
get("HK").has_postal_code; // false — Hong Kong has no postal codes
validate("", "HK"); // true — so empty is the correct value
validate("12345", "ZZ"); // undefined — NOT false. See below.undefined is not false
An unknown country returns undefined, deliberately. The most common way a validator
hurts a real user is by rejecting a correct address for a country the developer never
listed — so this library refuses to guess. Branch on it:
const ok = validate(code, country);
if (ok === false) return "That does not look like a valid postal code.";
// undefined → we have nothing to say about this country. Accept the input.What "valid" means here
Structure, and only structure. These patterns answer "could the postal service read this?" — not "does this place exist?". A code can be perfectly well-formed and completely unassigned. Nothing here is a delivery-point lookup, and none of it should gate anything that matters on its own.
Coverage
| | Country | Local name | Format | Example | Regex | Real codes checked |
|---|---|---|---|---|---|---:|
| 🇦🇺 | Australia (AU) | Postcode | NNNN | 4110 | ^\d{4}$ | 127 |
| 🇦🇹 | Austria (AT) | Postleitzahl (PLZ) | NNNN | 6029 | ^\d{4}$ | 150 |
| 🇧🇪 | Belgium (BE) | Postcode / Code postal | NNNN | 6000 | ^\d{4}$ | 147 |
| 🇧🇷 | Brazil (BR) | CEP | NNNNN-NNN | 33400-000 | ^\d{5}-?\d{3}$ | 50 |
| 🇨🇦 | Canada (CA) | Postal code | ANA NAN | B2V 1B1 | ^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$ | 149 (prefix) |
| 🇩🇰 | Denmark (DK) | Postnummer | NNNN | 5290 | ^\d{4}$ | 90 |
| 🇫🇮 | Finland (FI) | Postinumero | NNNNN | 45150 | ^\d{5}$ | 148 |
| 🇫🇷 | France (FR) | Code postal | NNNNN | 51430 | ^\d{5}$ | 113 |
| 🇩🇪 | Germany (DE) | Postleitzahl (PLZ) | NNNNN | 42275 | ^\d{5}$ | 150 |
| 🇭🇰 | Hong Kong (HK) | — | no postal code system | — | — | — |
| 🇭🇺 | Hungary (HU) | Irányítószám | NNNN | 6727 | ^\d{4}$ | 126 |
| 🇮🇳 | India (IN) | PIN code | NNNNNN | 400018 | ^\d{6}$ | 148 |
| 🇮🇩 | Indonesia (ID) | Kode pos | NNNNN | 30253 | ^\d{5}$ | 145 |
| 🇮🇪 | Ireland (IE) | Eircode | ANN AAAA | D01 F4E2 | ^[AC-FHKNPRTV-Y]\d[0-9W] ?[0-9AC-FHKNPRTV-Y]{4}$ | 12 (prefix) |
| 🇮🇹 | Italy (IT) | CAP | NNNNN | 37130 | ^\d{5}$ | 150 |
| 🇯🇵 | Japan (JP) | 郵便番号 (yūbin bangō) | NNN-NNNN | 100-0001 | ^\d{3}-?\d{4}$ | — (none) |
| 🇲🇽 | Mexico (MX) | Código postal | NNNNN | 52985 | ^\d{5}$ | 134 |
| 🇳🇱 | Netherlands (NL) | Postcode | NNNN AA | 1011 AB | ^\d{4} ?[A-Z]{2}$ | 150 (prefix) |
| 🇳🇴 | Norway (NO) | Postnummer | NNNN | 4634 | ^\d{4}$ | 150 |
| 🇵🇱 | Poland (PL) | Kod pocztowy | NN-NNN | 44-114 | ^\d{2}-?\d{3}$ | 150 |
| 🇵🇹 | Portugal (PT) | Código postal | NNNN-NNN | 3810-066 | ^\d{4}-?\d{3}$ | 150 |
| 🇷🇴 | Romania (RO) | Cod poștal | NNNNNN | 410320 | ^\d{6}$ | 150 |
| 🇷🇺 | Russia (RU) | Почтовый индекс | NNNNNN | 425411 | ^\d{6}$ | 145 |
| 🇰🇷 | South Korea (KR) | 우편번호 | NNNNN | 37727 | ^\d{5}$ | 150 |
| 🇪🇸 | Spain (ES) | Código postal | NNNNN | 33205 | ^\d{5}$ | 150 |
| 🇸🇪 | Sweden (SE) | Postnummer | NNN NN | 582 56 | ^\d{3} ?\d{2}$ | 150 |
| 🇨🇭 | Switzerland (CH) | Postleitzahl / NPA | NNNN | 4002 | ^\d{4}$ | 130 |
| 🇹🇷 | Turkey (TR) | Posta kodu | NNNNN | 27410 | ^\d{5}$ | 120 |
| 🇺🇦 | Ukraine (UA) | Поштовий індекс | NNNNN | 49112 | ^\d{5}$ | 143 |
| 🇬🇧 | United Kingdom (GB) | Postcode | AN NAA / ANN NAA / AAN NAA / AANN NAA / ANA NAA / AANA NAA | B1 1BB | ^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$ | 131 (prefix) |
| 🇺🇸 | United States (US) | ZIP Code | NNNNN or NNNNN-NNNN | 55810 | ^\d{5}(-\d{4})?$ | 5292 |
sample_kind is full unless marked. (prefix) means the sample file holds the
published geographic half of the code — a Canadian FSA, a UK outward code, an Eircode
routing key, a Dutch four-digit block — and the example column shows one of those
prefixes completed into a full code. (none) means no sample file: Japan's open postal
data is romanised, and this project's Japanese addresses are native script, so the two
were never joined honestly enough to publish.
The data
Each entry:
{
"iso2": "IE",
"country": "Ireland",
"local_name": "Eircode",
"has_postal_code": true,
"mask": "ANN AAAA",
"regex": "^[AC-FHKNPRTV-Y]\\d[0-9W] ?[0-9AC-FHKNPRTV-Y]{4}$",
"example": "D01 F4E2",
"samples": 12,
"sample_kind": "prefix",
"observed_masks": ["ANN"],
"note": "Introduced in 2015. The three-character routing key is geographic; the four-character identifier resolves to ONE delivery point, so two neighbouring houses have different Eircodes. The samples are routing keys."
}observed_masks is the shape of the sample codes as measured, not as asserted — it is
there so you can see the data disagreeing with the mask field if it ever does.
Testing your own forms against this
The sample files are the useful part for test suites: real codes, one per line,
data/samples/<ISO2>.txt. They make good fixtures precisely because
they are not uniform — Sweden's carry a space, Poland's a hyphen, Portugal's four
digits then three.
import { readFileSync } from "node:fs";
const codes = readFileSync("node_modules/postal-code-formats/data/samples/SE.txt", "utf8")
.split("\n").filter(Boolean);
// → ["100 04", "103 16", "105 47", ...]Provenance and limits
The sample codes come from GeoNames postal data (CC BY 4.0), joined to its population-ranked city list, as used in production by Fakenamely. Patterns and the per-country notes were written against the national postal authorities' own published standards (USPS Publication 28, Canada Post, Royal Mail, An Post, PostNL and others).
Honest limits, so you can decide whether this is enough for your case:
- 31 countries, not ~250. These are the ones with sample data good enough to test against. A short list that is verified beats a long one that is not — but if you need full ISO coverage, this is not that.
- A snapshot, not a feed. Postal systems change: Germany renumbered in 1993, Japan in
1998, Korea in 2015, Ireland went from no codes at all to per-door codes. This data is
dated in
data/postal-formats.jsonand will drift. - Samples are a slice. ~150 codes per country (5,292 for the US), not a directory. They exist to test the patterns, not to enumerate a country.
- Not affiliated with any postal authority.
Contributing
Corrections are very welcome, especially from people who live with a postal system
day to day. A pull request that changes a pattern should come with sample codes it
accepts that the old one rejected (or vice versa) — npm test will hold you to it.
Adding a country means adding data/samples/<ISO2>.txt and one entry in
data/postal-formats.json; the test suite checks the rest.
License
MIT for the code and the compiled dataset. The underlying GeoNames postal data is
CC BY 4.0 — attribution to GeoNames
carries over to anything derived from data/samples/.
