Contents
Changes
2.1.0 (2026-10-10)
Upgrading from 2.0.x or 1.3.x: ALTER EXTENSION postcode UPDATE;. Nothing stored changes; 2.1.0 only adds. (A rehearsal on a copy of the production database took 123 ms and left 21 million postcodes bit-for-bit identical.)
New: named parts. A pattern can name the parts of a code, (?<name>...), and the parts can be read back:
SELECT add_country_template('XQ', '/(?<major>\d{2})-(?<minor>\d{3})/');
SELECT part('XQ-12-345'::postal_code, 'major'); -- 12
SELECT parts('XQ-12-345'::postal_code); -- {"major": "12", "minor": "345"}
SELECT prefix_of('XQ-12-345'::postal_code, 'major'); -- the range of every code that starts XQ-12
part(code, name)is the piece of text;prefix_of(code, name)is everything up to and including it, as apostal_code_range(indexable, groupable, usable with<@). For GB these are the piecesarea,district,sectorandwalk(asto_charnames them) and Royal Mail’s levels:part('GB-SW1A 1AA', 'district')is1A;prefix_of(..., 'district')is the range ofGB-SW1A.parts(code)is all of them asjsonb(a part the code does not have is null). The viewpostal_code_partslists the parts each country has, in order. GB (and GG, GI, IM, JE), US, CA, IE and BR have parts, and so do 29 of the built-in pattern countries (those whose codes really have two kinds of information in them: ES and TR province, KY island, MT locality, NL digits and letters, thebaseandextensionof CO CR IR LB MZ PT SA TW VE, thezip5andplus4of the US territories, the two blocks of JP PL CL BM SO, and AR). The other 142 pattern countries – one undivided number, or only grouped for display, or a code of two lengths, or a single fixed code – and FR, CZ and LU have none, andpart()says so.- The UK
postcodetype has the same four parts:part(pc, 'area'),parts(pc)andprefix_of(pc, 'district')(which returns the text the%operator takes). They are read from the type’s own fields, so they areIMMUTABLEand can be indexed. Everyto_charletter combination has a replacement; the README has the table. - Because two types now have
part(),parts()andprefix_of(), a bare string literal is ambiguous:part('SW1A 1AA', 'area')is “function part(unknown, unknown) is not unique”. Cast it; a column needs nothing. - A name changes nothing about which codes are valid or how they are stored or ranked, so nothing stored is touched.
- Names are labels, not rules: the same codes written or named differently relabel the current language of a
country instead of making a new version (which would make different values), and a language’s pattern may be
rewritten in place to any pattern that denotes exactly the same codes (
postal_code_same_codes(), an exact comparison). A different set of codes is still a new version. - A pattern whose parts could split some code two ways is refused when it is defined, with an example code. Up to 10 million codes every code is tried; above that an exact analysis of the pattern decides (up to 2,000 positions); beyond that a sample is checked and a notice says only some codes were.
- A part must match at least one character and cannot be repeated (
{2}); lookbehind is refused. Templates cannot name parts: write a regular expression. make installcheckgains apartstest; the standalone tests gain the named-parts engine, the ambiguity check, the GB parts against thepostcodelayout’s own fields (8.3 million codes) and the US, CA, IE and BR parts against their encoders.
Deprecated: to_char(postcode, text). It keeps working unchanged and will not be removed within 2.x, but it is
not a sensible way to take a code apart (its A D S W letters give pieces, not Royal Mail’s district and sector).
Use part() / prefix_of(). The function carries a COMMENT saying so; there is no run-time warning, since it is
used in queries and a warning per call would flood logs.
- A change that could surprise: re-adding a pattern that denotes the same codes as a country’s current language, written differently, no longer makes a new version (which made different values). For your own language it relabels it; for a built-in language it says so in a NOTICE and keeps it. The very spec a language was made from is still silent, as in 2.0.x.
- Fixed: a use-after-free when compiling a large pattern (present since 2.0.0). The compiler kept a pointer into an array
that it then grew, so after a reallocation it read freed memory. On glibc the freed block still held the right data and the
pattern came out right; on macOS the real SIC 2007 list, written out as a pattern, came out with 34 of its 729 codes
missing. Found by compiling that list on a Mac and then under AddressSanitizer. No stored value is affected: the
largest built-in pattern has 17 states, none of the 171 triggers it (the same totals, state counts and sampled codes with and
without the fix, and no error under AddressSanitizer), and the production database has no patterns of its own. Only a pattern
with hundreds of states – a long list written out as an alternation – could have been wrongly compiled. New:
test_postal_code_lists.c(fails without the fix), and the engine tests now also run under AddressSanitizer and UBSan in CI. - The error for a statement that is not
(?: )or(?<name> )grouping now mentions named parts.
2.0.1
Upgrading from 2.0.0 or 1.3.x: ALTER EXTENSION postcode UPDATE;
is_valid(text)andis_valid(text, text)are replaced by one function,is_valid_postal_code(postcode text, cc text DEFAULT NULL). Same arguments and results; the country is now optional. Callers ofis_valid(...)must change. A bareis_validcollided with another extension’sis_valid(text)(gsscode, installed in the first production database 2.0.0 was tried on) and, wherever theisnextension is installed, was ambiguous for an untyped literal (is_valid('US-90210')failed with “function is_valid(unknown) is not unique”).- A database still on 1.3.x upgrades directly to 2.0.1 and never creates
is_valid; a database on 2.0.0 has the two old functions dropped and the new one created. Nothing else changes: the data, the types and the other functions are as in 2.0.0.
2.0.0
Upgrading from 1.3.x: ALTER EXTENSION postcode UPDATE;. The postcode and dps types, their
functions, operators and operator classes are unchanged, and stored values are untouched. 2.0.0 only adds.
New: postal_code, a 64-bit type for the postal codes of any country (see README.md).
- Written
CC-codeas the UPU recommends:US-90210,GB-SW1A 1AA,FR-75008. Values sort by country in ISO 3166-1 order, then by their own code; a coarser value sorts just before the finer ones that extend it (an outcode, a ZIP5 before its ZIP+4s). - Formats for 194 of the 250 ISO countries and territories; the other 56 have no postal codes
(
SELECT * FROM postal_code_world). Eight formats are compiled (US, CA, FR, BR, CZ, LU, GB, IE); the rest are patterns – a template or a bounded regular expression that defines the whole set of a country’s codes (a code is stored as its rank in that set, so order, validity and prefix ranges are exact) – and a new country is added with SQL:add_country_template('PL', 'NN-NNN')oradd_country_template('NL', '/[1-9]\d{3}( [A-Z]{2})?/'). Country rules narrower than a code’s shape are built in (NARROWING.md); the UK format enforces Royal Mail’s letter rules. - Prefix matching as a btree range scan:
postal_prefix(),to_postal_prefix(),lower_bound(),upper_bound(), thepostal_code_rangetype, and the%/!%operators.outcode()/district(). is_valid()(renamedis_valid_postal_code()in 2.0.1) and the NULL-returningto_postal_code()for loading dirty feeds. Input tolerates the spellings real data uses (other scripts' digits, Unicode dashes and spaces, missing hyphens, the country’s own letters in front) and nothing else.- A column can be locked to a country:
pc postal_code('US'). - Country assignments are data (
postal_code_country_formats,add_country_format(),remove_country_format()), with shipped and user assignments kept apart so that a dump carries yours.
Compatibility: PostgreSQL 14 or later. The regression suite passes on 14, 15, 16, 17 and 18 (built from
the release archive); .github/workflows/test.yml runs it on all five. One difference: from PostgreSQL 17
the planner turns pc <@ postal_prefix(...) into an index scan; on 14 to 16 it is correct but a filter,
and pc % '...' or pc >= lower_bound(...) AND pc < upper_bound(...) is the indexed form (see README.md).
Things to know
- Restoring a dump that contains
postal_codevalues for countries you assigned needs the extension’s own configuration tables loaded first;pg_restoreloads data in name order. A reordered restore list is documented in the README. Values for the built-in countries restore with no special handling. postal_code_inand the two-argument constructor areSTABLE, notIMMUTABLE, because they depend on the country table, so they cannot be used in an index expression. Indexing apostal_codecolumn is unaffected.- A pattern is permanent once a value has been written under it (each country has 51 versions); to change a
country’s rules, assign it a new pattern. See
README.md. - Where separators are dropped from UK outcode or sector text,
SW11is read as the outcodeSW11, notSW1 1; a full postcode is unambiguous. - Validated on 117 million OpenStreetMap postcodes, 1.8 million GeoNames codes and 24,000 Companies House
addresses;
VALIDATION.mdhas the method, results, and the known gaps.
1.3.1 to 1.3.5
Bug fixes by John Burns; each upgrade script (postcode--1.3.x--1.3.y.sql) is the record.
1.3.0
The original release, by Dave Green (patchsoft).