Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
6cc5c6a
feat: add Slovakia postal geocoding
jkrenge Nov 4, 2026
16de9dc
fix: refresh Austrian postal-code representative points
jkrenge Nov 4, 2026
ac17aad
feat: add Swiss postal geocodes from swisstopo
jkrenge Nov 4, 2026
87b4812
fix: use official France postal-code representative points
jkrenge Nov 4, 2026
0221eff
fix: refresh DEU postal geocoding points
jkrenge Nov 4, 2026
0669f5d
fix: use representative postal-code points for Spain
jkrenge Nov 4, 2026
a8ca359
feat: add Serbia postal geocoding
jkrenge Nov 4, 2026
7319519
feat: add Luxembourg postal geocoding
jkrenge Nov 4, 2026
9d1f93f
fix: refresh BEL postal-code representative points
jkrenge Nov 4, 2026
38d6da6
feat: add Swedish postal code geocoding
jkrenge Nov 4, 2026
1745dd6
feat: add KOR postal coordinate lookup
jkrenge Nov 4, 2026
b92dc74
feat: add Hungary postal geocoding
jkrenge Nov 4, 2026
59fa74e
feat: add Portugal postal coordinate lookup
jkrenge Nov 4, 2026
a02b6c7
feat: add Irish routing-key coordinate lookup
jkrenge Nov 4, 2026
35b9658
feat: add approximate AUS postcode coordinates
jkrenge Nov 4, 2026
253576c
fix: refresh NLD postal geocoding and normalize full codes
jkrenge Nov 4, 2026
18713b9
feat: add Finland postal-area geocoding
jkrenge Nov 4, 2026
f07a74f
feat: add Lithuania postal geocoding
jkrenge Nov 4, 2026
56a959e
feat: add Japan postal geocodes
jkrenge Nov 4, 2026
8fa4cb5
feat: add Romanian postal coordinate lookup
jkrenge Nov 4, 2026
35a1ea7
fix: refresh Italy postal geocodes with representative points
jkrenge Nov 4, 2026
b86d5ef
feat: add Mexico postal coordinate lookup
jkrenge Nov 4, 2026
5c0473c
fix: refresh GBR district geocoding and postcode formats
jkrenge Nov 4, 2026
cc5b202
fix: refresh Canadian FSA geocoding and normalize postal codes
jkrenge Nov 4, 2026
fb5c19f
fix: refresh USA geocodes and support ZIP+4 lookup
jkrenge Nov 4, 2026
73ab140
test: correct GBR legacy Jersey lookup expectation
jkrenge Nov 4, 2026
e724cb7
feat: add Poland postal geocoding
jkrenge Nov 4, 2026
8ed3d30
feat: add Danish postal geocoding
jkrenge Nov 4, 2026
5b7a754
feat: add Czech postal geocoding
jkrenge Nov 4, 2026
556861d
feat: add Norwegian postal geocoding
jkrenge Nov 4, 2026
1e54949
chore: integrate postal geocoding PR #95
jkrenge Nov 4, 2026
0fdaea7
chore: integrate postal geocoding PR #96
jkrenge Nov 4, 2026
6b73aee
chore: integrate postal geocoding PR #97
jkrenge Nov 4, 2026
fa19d77
chore: integrate postal geocoding PR #98
jkrenge Nov 4, 2026
a211037
chore: integrate postal geocoding PR #99
jkrenge Nov 4, 2026
1975e0d
chore: integrate postal geocoding PR #100
jkrenge Nov 4, 2026
b6caa89
chore: integrate postal geocoding PR #101
jkrenge Nov 4, 2026
50a8caf
chore: integrate postal geocoding PR #102
jkrenge Nov 4, 2026
646749c
chore: integrate postal geocoding PR #103
jkrenge Nov 4, 2026
611f708
chore: integrate postal geocoding PR #104
jkrenge Nov 4, 2026
ec4c88d
chore: integrate postal geocoding PR #105
jkrenge Nov 4, 2026
eb1f0cb
chore: integrate postal geocoding PR #106
jkrenge Nov 4, 2026
254b389
chore: integrate postal geocoding PR #107
jkrenge Nov 4, 2026
e633cf0
chore: integrate postal geocoding PR #108
jkrenge Nov 4, 2026
ce61771
chore: integrate postal geocoding PR #109
jkrenge Nov 4, 2026
b344edc
chore: integrate postal geocoding PR #110
jkrenge Nov 4, 2026
cad02e0
chore: integrate postal geocoding PR #111
jkrenge Nov 4, 2026
299f1fc
chore: integrate postal geocoding PR #112
jkrenge Nov 4, 2026
81e73ef
chore: integrate postal geocoding PR #113
jkrenge Nov 4, 2026
8a94aef
chore: integrate postal geocoding PR #114
jkrenge Nov 4, 2026
ac57dae
chore: integrate postal geocoding PR #115
jkrenge Nov 4, 2026
58c1352
chore: integrate postal geocoding PR #116
jkrenge Nov 4, 2026
c9b3b86
chore: integrate postal geocoding PR #117
jkrenge Nov 4, 2026
fa74d21
chore: integrate postal geocoding PR #118
jkrenge Nov 4, 2026
b65d097
chore: integrate postal geocoding PR #119
jkrenge Nov 4, 2026
cd322fe
chore: integrate postal geocoding PR #120
jkrenge Nov 4, 2026
b33f961
chore: integrate postal geocoding PR #121
jkrenge Nov 4, 2026
5ee089e
chore: integrate postal geocoding PR #122
jkrenge Nov 4, 2026
b54213a
chore: integrate postal geocoding PR #123
jkrenge Nov 4, 2026
c89d25b
feat: consolidate postal geocoding across 29 countries
jkrenge Nov 4, 2026
9a296c2
perf: compact geocodes to four significant digits
jkrenge Nov 5, 2026
035e7f6
refactor: use Node.js for Finland and UK geocode generators
jkrenge Nov 5, 2026
e876c14
refactor: port postal geocode generators to JavaScript
jkrenge Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ package-lock.json
country/**/*.json
regionNames/**/*.json
regions/**/*.json
geocode/**/*.json
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,9 @@ See [DATA_REQUIREMENTS.md](./DATA_REQUIREMENTS.md) for the country-data format a
## License

This module was built using adapted information from http://download.geonames.org/, which is registered under **CC BY 3.0**, as is this module. See http://creativecommons.org/licenses/by/3.0/ for more information.

## Geocode data precision and regeneration

All `geocode/*.json` coordinates are stored as strings rounded to four significant digits, with trailing zeros removed. Files use compact JSON without indentation. Postal codes, row order, and `[longitude, latitude]` return types are preserved. This is four significant digits, not four decimal places; longitude rounding can move a point by several kilometres.

After running any country generator, run `npm run geocode:compact`, then `npm test` and `npm run validate:data`. Country source notes describe the upstream data and aggregation before this final rounding step. Raw source fixtures and generator calculations retain their original precision.
43 changes: 43 additions & 0 deletions docs/geocode-AUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Australia postcode coordinates

## Source and license

- Publisher: [GeoNames postal-code download](https://download.geonames.org/export/zip/), file [AU.zip](https://download.geonames.org/export/zip/AU.zip).
- Retrieved: 2026-11-04. The archive members have timestamp 2026-11-04 03:53; this is an export date, not a survey date. Individual observation dates are not supplied.
- License: [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), as stated in the [postal readme](https://download.geonames.org/export/zip/readme.txt). Attribution: GeoNames, https://www.geonames.org/. This dataset is adapted by filtering and aggregation. Its CC BY 4.0 terms apply independently of the package's legacy license field.
- Input AU.txt SHA-256: `f5545c174ca9a248a0d191e144db8f975bbf0f13ee1d6239219e4f4ebfad3207`.
- GeoNames is the publisher, not Australia Post. Its [postal source table](https://www.geonames.org/postal-codes-sources.html) does not identify an official AU upstream source. This is a reusable approximate dataset, not an official postcode register.

## Coverage and method

The input contains 18,566 locality rows and 3,170 distinct four-digit postcodes. Retain only accuracy levels 4 (geonameid) and 6 (address or shape centroid) observed in this export. Exclude levels 1–3, including algorithmic estimates. This leaves 16,443 source rows and **3,044 output records / unique postcodes**. A code with no retained rows is absent, not replaced by a region or country point. National coverage is incomplete; 126 input codes are omitted, and completeness of the input is not established.

For each postcode, deduplicate numeric latitude/longitude pairs, sort them, and take the unweighted arithmetic mean of each axis. Round to six decimal places and store strings, consistent with legacy coordinate values. Repeated locality rows at the same point have no extra weight. The output is a mean of supplied points, not a postal-area, population, address-weighted, or first-locality centroid. Six decimal places are storage precision, not measured accuracy.

Coordinates use WGS84. Lookup returns `[longitude, latitude]`. AUS accepts exactly four digits after trimming surrounding whitespace. Leading zeros remain significant; `800`, `20 00`, and `2000 NSW` do not match. Country resolution remains unchanged. Unknown or omitted codes return `undefined`. Other countries keep their existing behavior. No public export or type changes are required; the coordinate function remains in `lib/geocode.js`.

## Primary-source sample checks and limits

Checked on 2026-11-04:

- Urban: Australia Post identifies [2000 as Sydney](https://auspost.com.au/postcode/2000). The output is approximately 151.2044 E, 33.8655 S, near the [UNGEGN Sydney reference](https://ungegn.un.org/dashboard/cities/details?id=539) (151.2167 E, 33.8833 S). This checks the city vicinity, not a postcode boundary or exact centroid.
- Leading zero: Australia Post identifies [0800 as Darwin City](https://auspost.com.au/postcode/0800). Both retained locality points are near 130.8416 E, 12.4610 S. The offline lookup test preserves the leading zero.
- Rural: Australia Post identifies [4482 as Birdsville](https://auspost.com.au/postcode/birdsville). Its GeoNames shape/address point is 139.8666 E, 25.8149 S, about 53 km from the [Queensland Government Birdsville aviation facility](https://www.tmr.qld.gov.au/community-and-environment/planning-and-development/planning-and-development-assessment-under-the-planning-act/strategic-airports-aviation-facilities-state-interest/location-aviation-facilities) (139.3535 E, 25.8959 S). It is not a town-center point. This demonstrates the material rural-distance limit.
- External islands: input 2899 (Norfolk), 6798 (Christmas), and 6799 (Cocos) have accuracy 1 and mainland coordinates. They are excluded. Lord Howe 2898 is retained at 159.0683 E, 31.5279 S; this does not establish complete territory coverage.

Large rural postcodes can span hundreds of kilometres (for example 0872). A single mean can fall away from any locality. PO box and special-use codes can also remain in the retained data. Do not use these points as proof of a physical scan location or a precise delivery address. No error bound, fraud decision, routing distance, or distance product is supplied.

## Regeneration and offline checks

Download AU.zip, extract AU.txt, and verify its hash for this snapshot. From the repository root:

```sh
node scripts/generate-aus-geocode.js /path/to/AU.txt > geocode/AUS.json
npx prettier --write geocode/AUS.json
npm test
npm run lint
npm run format:check
npm run validate:data
```

The generator rejects malformed country codes, postcode formats, blank/nonfinite/out-of-range coordinates, and invalid accuracy values. Tests check duplicate-row invariance, source-order invariance, filtering, invalid input, representative results, unknown codes, dataset uniqueness/ranges, and every generated record through the actual lookup. No network is needed for the tests.
33 changes: 33 additions & 0 deletions docs/geocode-AUT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Austria postal-code coordinates

## Source and license

- Source: [GeoNames Austria postal export](https://download.geonames.org/export/zip/AT.zip).
- Attribution: GeoNames, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), as stated in the [postal export readme](https://download.geonames.org/export/zip/readme.txt). No Austria-specific restriction is stated there. This derived file retains that attribution and license.
- Retrieved: 2026-11-04. HTTP Last-Modified: 2026-11-04 02:53:20 GMT. This is the export date, not a survey date.
- Archive SHA-256: `baff07a5779fec9e0b0355aa3dc39ed51dd5540da0acb626c78854a8db370f4c`.
- Source: 19,225 locality rows, 2,501 distinct four-digit postal codes. Output: 2,501 records. Previous file: 19,055 rows, 2,215 distinct codes.

## Method and compatibility

Run `node scripts/generate-aut-geocode.js AT.txt geocode/AUT.json` after extracting the archive. Then run `npm run geocode:compact`. The script validates each row, postal-code format, finite coordinates and coordinate ranges. Tests verify duplicate removal and preservation of a synthetic leading zero.

For each postal code, remove duplicate latitude/longitude pairs, then calculate the arithmetic mean of each coordinate. Each distinct point has equal weight. Sort codes and round to four decimal places. This deterministic representative point is not a postal-area centroid, a population-weighted center or a delivery address. Duplicate source rows do not add weight. A mean can fall between settlements.

The lookup retains string coordinates in `[longitude, latitude]` order. It accepts four-digit codes and the existing `1010 Wien` format. It does not add padding, country-prefix support or new normalization. Unknown codes return `undefined`. No runtime, exports, types or other country data change.

## Coverage and checks

The [GeoNames country page](https://www.geonames.org/countries/AT/austria.html) identifies the four-digit format. The export covers all nine states, but national completeness and current delivery status are not established. The [Austrian Post directory](https://www.post.at/g/c/postlexikon) includes current and historical codes; a source code does not establish an active delivery area.

Primary-source postal/locality checks:

- Urban: [City of Vienna](https://www.wien.gv.at/inhalt/impressum), 1010 Wien. Source point: 48.2085, 16.3721, in central Vienna.
- Rural: [Land Tirol](https://www.tirol.gv.at/gemeinden/gemeinde/70721), 9942 Obertilliach. Four source locality points give 46.7135, 12.6058, near Obertilliach.
- Border: [Statistics Austria street directory](https://www.statistik.at/verzeichnis/strassenliste/gemplzstr_8.pdf), 6991 Riezlern in Mittelberg. Source point: 47.3578, 10.1876, in Kleinwalsertal.

These checks confirm postal/locality associations and geographic plausibility, not measured positional error. Offline tests execute all 2,501 lookups, check unique codes and coordinate ranges, and pin these three examples.

## Limits

GeoNames states that coordinates can come from place-name matching or averages of neighboring postal codes. Its accuracy fields are heterogeneous; decimal precision is not positional accuracy. For example, 1300 is associated with Mannswörth in the export; do not interpret its point as the Vienna airport terminal. No special-code audit or comparison against a complete current Austrian Post list was done. Use these points only for approximate postal-level distance analysis. They cannot establish the distance of an individual parcel journey or support a fraud decision alone.
41 changes: 41 additions & 0 deletions docs/geocode-BEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Belgium postal-code points

## Source and license

- Data: [GeoNames BE.zip](https://download.geonames.org/export/zip/BE.zip), file `BE.txt`.
- Attribution: © GeoNames, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The [source readme](https://download.geonames.org/export/zip/readme.txt) permits redistribution with attribution. This file records the transformation; the source license continues to apply to the derived BEL data.
- Retrieved: 2026-11-04. The download index lists archive modification time as 2026-11-04 03:53 CET. This is an export date, not proof that every underlying observation is current. No per-record observation date is supplied.
- SHA-256 of extracted `BE.txt`: `b194ccb19c8358f859243596fa3e473919af9263ecbd0ff7830fc06278e25155`.

## Coverage and method

The source has 2,781 locality rows and 1,146 distinct four-digit postal codes. The output has 1,146 rows, one per postal code. It covers source-listed Belgian localities, not a verified complete national postal register. Leading zeros are retained as strings; the source has no leading-zero codes. Special code `0612` is not covered and returns `undefined`.

For each postal code, remove duplicate latitude/longitude pairs, then take the unweighted arithmetic mean of the distinct pairs. Sort pairs before summing for deterministic output. Round to six decimal places and store strings to preserve the existing return type. 516 codes have more than one distinct source point. This is a representative locality point, not a postal-area or population centroid. Duplicate source rows cannot add weight. No coordinates are invented for missing codes.

Rebuild with Node.js:

```sh
curl -fsS https://download.geonames.org/export/zip/BE.zip -o /tmp/BE.zip
unzip -o /tmp/BE.zip -d /tmp/BE
node scripts/generate-bel.js /tmp/BE/BE.txt
npm run geocode:compact
```

The generator checks the country, four-digit code format, and finite coordinate ranges. Tests verify that duplicates and input order do not change the average.

## Behavior and accuracy

`coordinates` still returns `[longitude, latitude]` as strings. Four-digit codes and code-plus-town input work through the existing space split. Country aliases and country-code case behavior are unchanged. Internal spaces, country prefixes, and truncated postal codes are not added as supported formats. Runtime code, exports, types, and other countries are unchanged.

The previous file had 3,386 rows and 1,175 unique codes; lookup selected the first locality. Updating to the current source removes these 29 codes, which now return `undefined`:

`1005`, `1006`, `1007`, `1008`, `1009`, `1010`, `1011`, `1012`, `1031`, `1041`, `1043`, `1044`, `1045`, `1047`, `1048`, `1049`, `1100`, `1105`, `1110`, `1201`, `1212`, `1414`, `1804`, `1818`, `1931`, `1934`, `4090`, `4151`, `7010`.

Their omission does not establish that they are invalid postal codes. The old file has no verified source provenance for retaining their coordinates. Do not substitute Brussels or another center for a missing code.

GeoNames describes its coordinates as estimates: some are matched to locality names and some can be inferred from neighboring postal codes. Six decimal places describe storage precision, not measured accuracy. The mean can lie between villages and has no validated error bound. Use only for approximate distance analysis, not a specific delivery point or a fraud decision.

## Primary-source checks

The [UPU Belgium addressing sheet](https://www.upu.int/UPU/media/upu/PostalEntitiesFiles/addressingUnit/belEn.pdf), supplied with bpost contact details, specifies four digits with no country prefix and gives `1000 Bruxelles` and `9990 Maldegem` as examples. [bpost's Burg-Reuland service-point page](https://www.bpost.be/en/points-of-sale/Lige/BURG-REULAND-4790/BURGREULAND) confirms rural border-area code `4790`. These locality/code checks agree with the source; they do not independently certify the coordinates. Offline tests pin the urban point, the two-point rural mean, and the high-end postal code, and execute lookup for every output row.
40 changes: 40 additions & 0 deletions docs/geocode-CAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# CAN postal-code geocoding

## Source and license

The dataset is derived from [GeoNames CA.zip](https://download.geonames.org/export/zip/CA.zip), retrieved on 2026-11-04. The download index reports 2026-11-04 03:53 CET. This is an export timestamp; the dates of the underlying observations are unknown.

GeoNames licenses its postal files under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), as stated in its [postal-data readme](https://download.geonames.org/export/zip/readme.txt). Credit: [GeoNames](https://www.geonames.org/). This derived file filters records and combines repeated codes. Archive SHA-256: `c7e68ee67dec276ff04c825947100fcd485dffc98f46a544382d2c37a4c016b2`.

## Coverage and method

The source has 1,657 rows and 1,653 distinct postal strings. The output has **1,650 rows and 1,650 unique three-character forward sortation areas (FSAs)**, with records in all 13 provinces and territories. Coverage is limited to the source; this is not a complete or current Canada Post postal-code register.

- Exclude `H0H`: its source point is latitude 90, longitude 0. Canada Post uses [H0H 0H0 for its Santa program](https://www.canadapost-postescanada.ca/cpc/en/our-company/write-letter-to-santa.page). It is unsuitable for physical-distance estimates.
- Exclude full-code anomalies `T3T 0E5` and `V3Y 0H2`. Do not turn an isolated full-code point into an FSA point. `V3Y` has a separate FSA record; `T3T` remains unsupported.
- For each FSA, deduplicate latitude/longitude pairs. Take the arithmetic mean of the distinct pairs, in sorted order, rounded to six decimal places. This is a representative mean of source points, **not a postal-area, address-weighted, or population-weighted centroid**. Repeated rows do not increase a point's weight.
- `L9X` has four distinct points; its result is latitude 44.443175, longitude -79.77035. `C0A` has two rows with the same point, so its coordinates stay unchanged.
- Preserve `[longitude, latitude]` string results. Coordinates use WGS84. Decimal places do not establish positional accuracy. Validate finite values, geographic ranges, code syntax and output uniqueness during generation and offline lookup tests.

To regenerate, extract the pinned archive, run `node scripts/generate-can.js /path/to/CA.txt`, then run `npm run geocode:compact`. The script checks source counts and exclusions. Tests verify that duplicates do not add weight. Review a later source release before changing its assertions.

Compared with the legacy file, this refresh removes unsupported `L0V`, removes the H0H point, adds source FSAs, updates changed source coordinates, and replaces duplicate rows with one representative point. It does not change region identification.

## Lookup behavior and checks

CAN accepts an FSA or a syntactically valid full postal code, in spaced or compact form, with either case and optional surrounding whitespace. Full codes resolve to their FSA point. The suffix is checked for syntax, not existence. Unknown FSAs and invalid input return `undefined`; there is no country-center fallback. Other countries keep their existing normalization.

Primary-source checks on 2026-11-04:

| Case | Evidence and interpretation |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Urban | The [House of Commons](https://www.ourcommons.ca/en/contact-us) gives Ottawa K1A 0A6. K1A resolves to 45.4207, -75.7023, in Ottawa. This verifies regional plausibility, not building accuracy. |
| Rural FSA | [St. Paul](https://www.stpaul.ca/residents) gives T0A 3A0. T0A resolves to the source's Eastern Alberta point, 54.766, -111.7174. This is a broad-area estimate, not the town office location. |
| Northern edge | [Whitehorse](https://www.whitehorse.ca/contact-us/) gives Y1A 1C2. Y1A resolves to 60.7161, -135.0537, near the [official Whitehorse place-name point](https://geonames.nrcan.gc.ca/search-place-names/unique?id=KAHFT), 60°43′27″ N, 135°3′22″ W. |
| Special code | Canada Post's Santa code above has no usable physical-distance point and returns `undefined`. |

## Accuracy limits

[Statistics Canada](https://www150.statcan.gc.ca/n1/pub/92-179-g/92-179-g2021001-eng.htm) defines FSAs as the first three postal characters and distinguishes census FSAs from Canada Post delivery areas. Its census boundary product is not the source of these coordinates.

GeoNames uses estimated and locality-derived points as well as some centroid records. No national error bound is supplied. Rural and northern FSAs can span very large areas: X0A's source point is 79.6218, -76.7765 and must not be interpreted as Iqaluit's position. Two distant full codes in the same FSA return the same point and therefore produce zero distance. Distances between these representative points cannot establish travel distance, drop-off location, or fraud. No full-code accuracy or national completeness is claimed.
Loading
Loading