Poland (PL) build pipeline
Single-source GeoNames PL rebuild. Self-contained: every script resolves its paths relative to itself and fetches directly from public sources.
Run order
python3 fetch_source.py # GeoNames PL.txt + sources/manifest.json
python3 build_base.py # -> data/pl.csv (TERYT preserved, ISO codes derived)
python3 recover_alt_names.py --fetch # restores alternative_city_name from the R2 copy
python3 integrity_checker.py # writes data/consolidated_stats.json; must be PASS
python3 package.py # datapackage.json + README.md + ATTRIBUTION.txt + pl.zip
python3 publish_r2.py # dry run by default; --apply to publish
recover_alt_names.py --fetch and publish_r2.py need the postal-codes repo .env
(R2_ACCESS_KEY, R2_SECRET_KEY, R2_ENDPOINT_URL) — the EU-jurisdiction endpoint.
See docs/R2_ACCESS.md in the fedex repo; the two repos use different variable names.
Sources
- GeoNames
PLpostal export — CC BY 4.0. Attribution is a licence condition and is carried indata/ATTRIBUTION.txt,data/README.mdanddatapackage.json.
Poczta Polska is not usable and must not be reintroduced. The Oficjalny Spis PNA is a
commercial product whose terms permit use "for the customer's own purposes … without the
right to resale to third parties" — including the freely-downloadable PDF edition. See
../.licensing-notes.md.
GUS TERYT was assessed and deliberately not used: it is commercially reusable under the
Polish public-sector-information re-use act, but its download page is ASP.NET post-back
only (no direct file URLs, fetch_tier: dynamic) and GeoNames already carries the TERYT
powiat and gmina codes this build needs.
What this rebuild fixes
The R2 copy this replaces was an unpackaged raw parse that round-tripped the data through pandas and destroyed the official codes:
| field | GeoNames | old R2 file | this build |
|---|---|---|---|
admin_code2 (TERYT powiat) | 0201 | 201.0 | 0201 |
admin_code3 (TERYT gmina) | 020102 | 20102.0 | 020102 |
admin_code1 | 72 (source-internal) | 72 | DS (ISO 3166-2:PL) |
admin_name1 | Kujawsko-Pomorskie | Kujawsko Pomorskie | Kujawsko-Pomorskie |
| empty admin cells | empty | nan (string) | empty |
Row counts and column names agreed throughout, which is why the corruption shipped.
integrity_checker.py therefore checks values, not shape — the load-bearing assertion is
that the TERYT gmina code always extends the powiat code (020102.startswith(0201)).
A stripped leading zero breaks that relation, so it cannot pass silently again.
Verification (2026-08-27)
Cold fetch → build → check → package, all green:
- 72,899 rows, 20,299 distinct postal codes, zero duplicate primary keys.
- 16/16 voivodeships. The ISO mapping is derived from the TERYT prefix and cross-checked three ways: GeoNames region number ↔ TERYT voivodeship prefix ↔ region name must be 1:1:1 on every row, or the build aborts.
- 380 powiats — matches Poland's real count (314 land powiats + 66 city powiats).
- 2,476 gminas of 2,477 — the one absent gmina is the single source row that carries
no TERYT code at all (
88-420 Jeziora), left blank rather than guessed. - 10,268 alternative names recovered; 14,752 legacy values that merely echoed
place_namewere dropped as carrying no information. - Frictionless validation passed against the packaged resource.
One upstream conflict, reported not hidden: Pilów / 78-650 appears under two
gminas (Wałcz 321705 and Mirosławiec 321703), 90 m apart. Both are genuine source
rows, so admin_code3 is part of the primary key and both are kept. integrity_checker.py
prints any such split rather than collapsing it.
Not run
publish_r2.py --apply has not been run. The R2 objects and _meta/catalog.json
still hold the old single-file version.