# 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, `(?...)`, and the parts can be read back: SELECT add_country_template('XQ', '/(?\d{2})-(?\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 a `postal_code_range` (indexable, groupable, usable with `<@`). For GB these are the pieces `area`, `district`, `sector` and `walk` (as `to_char` names them) and Royal Mail's levels: `part('GB-SW1A 1AA', 'district')` is `1A`; `prefix_of(..., 'district')` is the range of `GB-SW1A`. * `parts(code)` is all of them as `jsonb` (a part the code does not have is null). The view `postal_code_parts` lists 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, the `base` and `extension` of CO CR IR LB MZ PT SA TW VE, the `zip5` and `plus4` of 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, and `part()` says so. * **The UK `postcode` type has the same four parts**: `part(pc, 'area')`, `parts(pc)` and `prefix_of(pc, 'district')` (which returns the text the `%` operator takes). They are read from the type's own fields, so they are `IMMUTABLE` and can be indexed. Every `to_char` letter combination has a replacement; the README has the table. * Because two types now have `part()`, `parts()` and `prefix_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 installcheck` gains a `parts` test; the standalone tests gain the named-parts engine, the ambiguity check, the GB parts against the `postcode` layout'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 `(? )` grouping now mentions named parts. ## 2.0.1 **Upgrading from 2.0.0 or 1.3.x:** `ALTER EXTENSION postcode UPDATE;` * **`is_valid(text)` and `is_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 of `is_valid(...)` must change. A bare `is_valid` collided with another extension's `is_valid(text)` (`gsscode`, installed in the first production database 2.0.0 was tried on) and, wherever the `isn` extension 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-code` as 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')` or `add_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()`, the `postal_code_range` type, and the `%` / `!%` operators. `outcode()` / `district()`. * `is_valid()` (renamed `is_valid_postal_code()` in 2.0.1) and the NULL-returning `to_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_code` values for countries *you* assigned needs the extension's own configuration tables loaded first; `pg_restore` loads 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_in` and the two-argument constructor are `STABLE`, not `IMMUTABLE`, because they depend on the country table, so they cannot be used in an index expression. Indexing a `postal_code` column 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, `SW11` is read as the outcode `SW11`, not `SW1 1`; a full postcode is unambiguous. * Validated on 117 million OpenStreetMap postcodes, 1.8 million GeoNames codes and 24,000 Companies House addresses; `VALIDATION.md` has 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).