ID giả > Bài viết > Postal Codes of the World: Why a Universal Address Generator Cannot Exist

Bài viết này chưa được dịch sang Tiếng Việt — bạn đang đọc bản gốc bằng English. Cũng có bằng: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.

ShapeLocalesExamples
99999 (5 digits)22en_US 10001 · de_DE 10115 · fr_FR 75001 · tr_TR 34000
9999 (4 digits)20da_DK 1050 · en_AU 2000 · hu_HU 1051 · ka_GE 0105
999999 (6 digits)6en_IN 400001 · ru_RU 101000 · zh_CN 100000 · vi_VN 700000
999 (3 digits)2is_IS 101 · zh_TW 100
9999999 (7 digits)1he_IL 6100000
9999999999 (10)1fa_IR 1116843734
Contains letters or separators12see 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

LocaleShapeExampleWhat the extra structure is
en_CA / fr_CAA9A 9A9M5H 2N2alternating letters and digits
nl_NL9999 AA1011 ABdigits then a two-letter suffix
lv_LVAA-9999LV-1050country prefix, then four digits
ro_MDAA-9999MD-2001country prefix, then four digits
cs_CZ999 99110 00five digits split 3+2 by a space
pl_PL99-99900-001five digits split 2+3 by a hyphen
ja_JP999-9999100-0001seven digits split 3+4
pt_PT9999-9991000-001seven digits split 4+3
pt_BR99999-99901000-000eight digits split 5+3
en_GBAA9 9AA · A9 9AA · AA99 9AA · AA9A 9AAEC1A 1BBfour shapes inside one country
en_IE16 distinct shapesD01 VFEKsee 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.

CountryCodeThe unit it identifies
IrelandA65 F4E2one building
Netherlands1011 ABa side of a street; with house number, one address
United KingdomEC1A 1BBa small cluster of addresses on one street
Japan100-0001a chōme — a city block-level district
United States10001a delivery route / post office service area
Iceland101a district of a town
Ugandanothing; 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

  1. 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.
  2. Look for placeholder columns. Uganda's 60 identical 00000 entries passed every structural check we had. A column with one distinct value across an entire locale is a signal, not data.
  3. Check the alphabet, not just the length. Ireland forbids eleven letters. A random alphanumeric string fails roughly a third of the time.
  4. 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.
  5. 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.
  6. 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 ⇥ Postcode triple. Pure-digit locales: 52. Locales containing letters or separators: 12.
  • Ireland — Eircode. 72 rows, 71 distinct routing keys, all matching ^[A-Z]\d\d$; D6W absent from our data. Format: 3-character routing key + 4-character unique property identifier. Permitted alphabet ACDEFHKNPRTVWXY0123456789 (25 characters; B G I J L M O Q S U Z excluded). 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 table NDQ07, dimension C03349V04063), published under CC BY 4.0; the authoritative address and finder databases, ECAD and ECAF, are commercial products licensed separately. <eircode.ie/&gt;
  • Uganda — en_UG. All 60 locality rows carry 00000. 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.php and adds exactly four draws to the seeded stream, only for en_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.

← Bài viết