Ten artykuł nie został jeszcze przetłumaczony na Polski — czytasz oryginał w języku English. Dostępny także w:Deutsch, English, Українська
Postal Codes of the World: Why a Universal Address Generator Cannot Exist
There is a function in almost every test-data library that looks like this:
function postcode() { return str_pad(rand(0, 99999), 5, '0', STR_PAD_LEFT); }
It is wrong in 42 of the 64 locales we ship.
Not "slightly unidiomatic" — structurally wrong, in the sense that the string it returns could not be a postal code in that country under any circumstances. And the reason is not that five is an unlucky number. It is that "postal code" is not one thing. It is a label we have stretched across several dozen unrelated national systems that encode different objects at different resolutions, and a few countries that have no such system at all.
We measured this across our own shipped data rather than arguing it in the abstract.
What 64 locales actually look like
Every locality file we ship is a City ⇥ Region ⇥ Postcode triple built from real sources. Reducing each postcode to a shape — digit → 9, letter → A — gives the distribution below.
| Shape | Locales | Examples |
|---|---|---|
99999 (5 digits) | 22 | en_US 10001 · de_DE 10115 · fr_FR 75001 · tr_TR 34000 |
9999 (4 digits) | 20 | da_DK 1050 · en_AU 2000 · hu_HU 1051 · ka_GE 0105 |
999999 (6 digits) | 6 | en_IN 400001 · ru_RU 101000 · zh_CN 100000 · vi_VN 700000 |
999 (3 digits) | 2 | is_IS 101 · zh_TW 100 |
9999999 (7 digits) | 1 | he_IL 6100000 |
9999999999 (10) | 1 | fa_IR 1116843734 |
| Contains letters or separators | 12 | see below |
Data as of 2026-07-18, measured on the shipped locality files.
Five digits is the single most common shape, and it is still a minority — 22 of 64, about 34%. A generator that emits five digits everywhere gets roughly a third of the world right and betrays itself in the other two thirds. And the spread is not a rounding difference: Iceland uses three digits, Iran uses ten. That is a 3.3× range in length alone.
The twelve that are not just digits
| Locale | Shape | Example | What the extra structure is |
|---|---|---|---|
en_CA / fr_CA | A9A 9A9 | M5H 2N2 | alternating letters and digits |
nl_NL | 9999 AA | 1011 AB | digits then a two-letter suffix |
lv_LV | AA-9999 | LV-1050 | country prefix, then four digits |
ro_MD | AA-9999 | MD-2001 | country prefix, then four digits |
cs_CZ | 999 99 | 110 00 | five digits split 3+2 by a space |
pl_PL | 99-999 | 00-001 | five digits split 2+3 by a hyphen |
ja_JP | 999-9999 | 100-0001 | seven digits split 3+4 |
pt_PT | 9999-999 | 1000-001 | seven digits split 4+3 |
pt_BR | 99999-999 | 01000-000 | eight digits split 5+3 |
en_GB | AA9 9AA · A9 9AA · AA99 9AA · AA9A 9AA | EC1A 1BB | four shapes inside one country |
en_IE | 16 distinct shapes | D01 VFEK | see the Irish section |
Look at Czech and Polish together. Both are five-digit systems. Both split the five digits. Czech splits them 3+2 with a space, Poland splits them 2+3 with a hyphen. There is no principle available to you that predicts which; you have to know. The same holds for Portugal (9999-999) against Japan (999-9999) — seven digits each, split in opposite places.
And notice what the separator is doing in Latvia and Moldova. LV- and MD- are not part of the code's information content; they are an ISO country prefix that these two countries write inline. Strip it and the code still works. Strip the space from a Dutch 1011 AB and you also still have a valid code — but strip the hyphen from a Japanese 100-0001 and you have a seven-digit string that Japan Post will not parse. Separators are sometimes cosmetic and sometimes load-bearing, and which one it is varies by country.
The Uganda case: 60 codes that are all 00000
One row in that first table is a lie, and it is worth showing rather than hiding.
en_UG sits in the 5-digit column. All 60 of its 60 entries are 00000. Uganda does not have a national postal code system. There is nothing to put in the field, so the field carries a placeholder, and a naive shape-analysis — including our own first pass — files Uganda under "five digits" alongside Germany and the United States.
This is the strongest possible argument for the article's thesis. The schema has a postcode column, so every country gets one, so every country appears to have postal codes. The data model manufactured the universality it was supposed to describe. The honest count of genuinely five-digit locales in our data is 21, not 22.
There are more countries in this position than developers expect — Ireland itself was one until very recently, which brings us to the most instructive system in the set.
Ireland: the code that identifies a building
Ireland had no national postal code system at all until July 2015. Dublin had numbered postal districts (D1 through D24), and everywhere else an address was a house name, a townland and a county — genuinely, with no code. The country ran its post that way for the whole of the twentieth century.
When Ireland finally built a system, it did not build the one everyone else has. An Eircode is seven characters in two parts that do fundamentally different jobs:
A65 F4E2
└┬┘ └─┬─┘
│ └── unique identifier for ONE property — no geography, no ordering
└─────── routing key — a geographic area
The first three characters are geographic. The last four are not. They identify an individual building, they are deliberately non-sequential, and they carry no locational information whatsoever. Two neighbouring houses on the same street have unrelated Eircodes. There is no such thing as "the Eircode for Galway."
Every assumption a postal-code generator normally relies on fails here at once. The code is not hierarchical in its second half. It cannot be interpolated. Neighbouring codes are not neighbouring places. You cannot derive one from a city, because a city has tens of thousands of them.
The alphabet is not the alphabet
Eircodes use 25 characters: the ten digits, and only 15 of the 26 letters. Excluded, deliberately, are:
B G I J L M O Q S U Z
Read that list next to the digits and the design becomes obvious. B/8, G/6, I/1, J/1, L/1, O/0, Q/0, S/5, Z/2 — every one of them is a character that gets misread as a digit, or as another letter, when handwritten or scanned. The Irish system removed the ambiguity at the design stage rather than handling it at the parsing stage.
The permitted set is exactly ACDEFHKNPRTVWXY0123456789. A generator that produces a random alphanumeric string will emit a forbidden character roughly a third of the time, and the result is not a rare or unusual Eircode — it is a string that no validator in Ireland will accept.
How we generate them
We ship 72 Irish rows carrying 71 distinct routing keys with full real Eircodes. If we used the stored code verbatim, every card generated for Dublin would carry the identical Eircode D01 VFEK — an instant, visible forgery in a country where neighbours have different codes.
So the runtime splits the code the way the standard splits it:
if ($locale === 'en_IE' && $postcode !== '') {
$rk = strtok($postcode, ' '); // routing key: real, from locality data
if ($rk !== false && $rk !== '') {
$EIR = 'ACDEFHKNPRTVWXY0123456789'; // 25 characters, Eircode alphabet
$uid = '';
for ($i = 0; $i < 4; $i++) $uid .= $EIR[mt_rand(0, 24)];
$postcode = $rk . ' ' . $uid;
}
}
The routing key stays authentic and stays consistent with the city. The four-character identifier — the part that is meaningless by design — is drawn character by character from the correct alphabet. The result is geographically truthful and individually distinct, which is precisely the shape of the real thing.
The 16 distinct patterns our Irish file shows are a consequence of this: the identifier mixes letters and digits freely, so A99 AAAA, A99 AA9A and A99 AAA9 are all just the same format observed at different draws.
The exception we do not carry
Irish routing keys follow letter-digit-digit — D01, K67, T12, P31. There is exactly one that does not: D6W, covering Dublin 6 West, which has a letter in the third position because Dublin 6W existed as a postal district before Eircode and had to be preserved.
We checked, and D6W is not in our file. All 71 routing keys we ship match ^[A-Z]\d\d$. This is not a bug in the generator — the routing key comes from the locality data and is emitted unchanged, so nothing malformed is produced. It is a gap in coverage: our Irish data cannot generate a Dublin 6W address, and any validator we wrote against the observed pattern letter-digit-digit would reject a real Eircode.
We are stating this because it is the kind of thing that gets quietly rounded off. The regex that matches 100% of your data is not the regex that matches the standard.
What each system actually encodes
Length is the shallow difference. The deeper one is that these systems are not measuring the same object.
| Country | Code | The unit it identifies |
|---|---|---|
| Ireland | A65 F4E2 | one building |
| Netherlands | 1011 AB | a side of a street; with house number, one address |
| United Kingdom | EC1A 1BB | a small cluster of addresses on one street |
| Japan | 100-0001 | a chōme — a city block-level district |
| United States | 10001 | a delivery route / post office service area |
| Iceland | 101 | a district of a town |
| Uganda | — | nothing; the system does not exist |
These are five different resolutions of the same nominal field. The US ZIP identifies a delivery route — it is an operational artefact of the Postal Service, which is why ZIP boundaries cross city and even state lines and why the +4 extension exists to get finer. The Japanese code descends to the chōme, a named block subdivision. The Dutch code plus a house number is a complete address, no street name required.
And the consequence for generated data is direct. Assigning one postal code per city is a defect whose visibility depends entirely on which country you are in. In the United States it is a mild simplification — a real ZIP for a real city, just not the only one. In Ireland it produces a physical impossibility: thousands of people living in one building.
What to check in any address generator
- Count the distinct shapes in your own data before you write a validator. Ours has seven pure-digit lengths and twelve locales with letters or separators. A single regex is not going to cover that.
- Look for placeholder columns. Uganda's 60 identical
00000entries passed every structural check we had. A column with one distinct value across an entire locale is a signal, not data. - Check the alphabet, not just the length. Ireland forbids eleven letters. A random alphanumeric string fails roughly a third of the time.
- Ask what the code identifies. If it resolves below the settlement — a building, a street side, a block — then one code per city is a structural error, not an approximation.
- Do not infer the standard from your sample. All 71 of our Irish routing keys are letter-digit-digit. The standard permits
D6W. Our sample was unanimous and still incomplete. - Treat separators as data until proven cosmetic. Czech splits five digits 3+2, Poland splits five digits 2+3, and nothing about the numbers tells you which.
The general point is the one that keeps recurring in this work. A field called postcode in a database schema looks like a single concept because the schema is a single column. The countries never agreed to that. The universality is in your data model, not in the world — and every generator that treats the column as the concept ends up producing addresses that are individually plausible and collectively impossible.
Data as of 2026-07-18
Format shapes were computed on 18 July 2026 directly from the 64 shipped locality files by mapping every Unicode digit to 9 and every Unicode letter to A, then counting distinct results per locale. Counts in the shape table are locales, not rows.
Sources and notes:
- Locality files — 64 locales, each row a
City ⇥ Region ⇥ Postcodetriple. Pure-digit locales: 52. Locales containing letters or separators: 12. - Ireland — Eircode. 72 rows, 71 distinct routing keys, all matching
^[A-Z]\d\d$;D6Wabsent from our data. Format: 3-character routing key + 4-character unique property identifier. Permitted alphabetACDEFHKNPRTVWXY0123456789(25 characters;B G I J L M O Q S U Zexcluded). Eircode launched July 2015; before that only Dublin postal districts existed. Our locality file stores the routing key only; the 4-character property identifier is generated at runtime. The routing keys come from the CSO's official Eircode Routing Key classification (PxStat tableNDQ07, dimensionC03349V04063), published under CC BY 4.0; the authoritative address and finder databases, ECAD and ECAF, are commercial products licensed separately. <eircode.ie/> - Uganda —
en_UG. All 60 locality rows carry00000. Uganda operates no national postal code system; the column is a placeholder. City population weights are also absent for this locale (UBOS 2014 parsed but not loaded). - United Kingdom —
en_GB. 52 rows across four shapes:AA9 9AA(41),A9 9AA(5),AA99 9AA(5),AA9A 9AA(1). - Generation code — the Irish branch lives in
ng_generate.phpand adds exactly four draws to the seeded stream, only foren_IE; other locales pass the branch untouched and their cards are not shifted. - Related reading — the companion problem of which city gets picked, and why uniform selection understates capitals by roughly a factor of three, is covered in Why Address Generators Produce Implausible Cities.