Skip to content

Commit 2f71c83

Browse files
dr5hnclaude
andcommitted
feat(postcodes/SJ): import 8 Svalbard + Jan Mayen codes (#1039)
Mirrors Svalbard (9170-9178) and Jan Mayen (8099) postcodes from the already-shipped NO.json into SJ's own country namespace. Why --- Svalbard archipelago and Jan Mayen Island are administered by Norway and use Norwegian post codes already shipped in NO.json under state codes 21 (Svalbard) and 22 (Jan Mayen). CSC represents Svalbard and Jan Mayen as their own country (iso2=SJ, country_id=211), so without a companion SJ.json, postcode lookups for SJ return empty. Coverage -------- - 8 codes / country-only ship (SJ has no sub-state hierarchy in CSC) - Svalbard: 9170, 9171 Longyearbyen; 9173 Ny-Ålesund; 9174 Hopen; 9175 Sveagruva; 9176 Bjørnøya; 9178 Barentsburg - Jan Mayen: 8099 Source pipeline --------------- 1. Read NO.json (Bring mirror) 2. Filter to 8 SJ-target codes 3. Re-FK to country_id=211 (SJ) 4. Title-case locality names from the bring-shipped uppercase form License ------- Original source: Bring (Norway Post) via Norway importer. Each row: source: "bring-via-sj-mirror" Validation ---------- - python3 -m py_compile passes - 100% regex match (^\d{4}$) - No state_id (country-only) - No auto-managed fields (id, created_at, updated_at, flag) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 085bfd5 commit 2f71c83

2 files changed

Lines changed: 218 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""Svalbard and Jan Mayen -> contributions/postcodes/SJ.json importer for issue #1039.
3+
4+
Source data
5+
-----------
6+
Svalbard archipelago and Jan Mayen Island are administered by
7+
Norway and use Norwegian post codes:
8+
9+
Svalbard codes 9170, 9171 Longyearbyen
10+
9173 Ny-Ålesund
11+
9174 Hopen weather station
12+
9175 Sveagruva (decommissioned)
13+
9176 Bjørnøya (Bear Island)
14+
9178 Barentsburg (Russian mining settlement)
15+
16+
Jan Mayen code 8099 Jan Mayen weather station
17+
18+
These codes are already shipped in contributions/postcodes/NO.json
19+
under Norwegian state 21 (Svalbard) and 22 (Jan Mayen). CSC
20+
represents Svalbard and Jan Mayen as their own country (iso2=SJ,
21+
country_id=211), so this importer mirrors the codes into a
22+
companion SJ.json under the SJ namespace.
23+
24+
What this script does
25+
---------------------
26+
1. Reads existing NO.json filtered to state codes 21 (Svalbard) +
27+
22 (Jan Mayen).
28+
2. Re-FKs each row to country_id=211 (SJ).
29+
3. Country-only ship — SJ has no sub-state hierarchy in CSC.
30+
31+
License & attribution
32+
---------------------
33+
- Original source: Bring (Norway Post) via the Norway importer
34+
(CC-BY-equivalent free-redistribution per #1039 license-tier policy)
35+
- Each row: ``source: "bring-via-sj-mirror"``
36+
37+
Usage
38+
-----
39+
python3 bin/scripts/sync/import_svalbard_jan_mayen_postcodes.py
40+
"""
41+
42+
from __future__ import annotations
43+
44+
import argparse
45+
import json
46+
import re
47+
import sys
48+
from pathlib import Path
49+
from typing import Dict, List
50+
51+
52+
SVALBARD_CODES = {"9170", "9171", "9173", "9174", "9175", "9176", "9178"}
53+
JAN_MAYEN_CODES = {"8099"}
54+
55+
56+
def main() -> int:
57+
parser = argparse.ArgumentParser(description=__doc__)
58+
parser.add_argument("--dry-run", action="store_true")
59+
args = parser.parse_args()
60+
61+
project_root = Path(__file__).resolve().parents[3]
62+
63+
countries = json.load(
64+
(project_root / "contributions/countries/countries.json").open(encoding="utf-8")
65+
)
66+
sj_country = next((c for c in countries if c.get("iso2") == "SJ"), None)
67+
if sj_country is None:
68+
print("ERROR: SJ not in countries.json", file=sys.stderr)
69+
return 2
70+
regex = re.compile(sj_country.get("postal_code_regex") or ".*")
71+
72+
no_path = project_root / "contributions/postcodes/NO.json"
73+
if not no_path.exists():
74+
print("ERROR: NO.json missing", file=sys.stderr)
75+
return 2
76+
no_data = json.load(no_path.open(encoding="utf-8"))
77+
78+
target_codes = SVALBARD_CODES | JAN_MAYEN_CODES
79+
sj_rows = [r for r in no_data if r.get("code") in target_codes]
80+
print(f"Source SJ-target rows in NO.json: {len(sj_rows)}")
81+
82+
seen: set = set()
83+
records: List[dict] = []
84+
skipped_bad_regex = 0
85+
86+
for r in sj_rows:
87+
code = r["code"]
88+
if not regex.match(code):
89+
skipped_bad_regex += 1
90+
continue
91+
92+
locality = r.get("locality_name") or ""
93+
# Title-case the bring-shipped uppercase locality
94+
if locality.isupper():
95+
locality = locality.title().replace("-Å", "-Å").replace("Ø", "Ø")
96+
key = (code, locality.lower())
97+
if key in seen:
98+
continue
99+
seen.add(key)
100+
101+
record: Dict[str, object] = {
102+
"code": code,
103+
"country_id": int(sj_country["id"]),
104+
"country_code": "SJ",
105+
}
106+
if locality:
107+
record["locality_name"] = locality
108+
for field in ("latitude", "longitude"):
109+
v = r.get(field)
110+
if v is not None and v != "":
111+
record[field] = v
112+
record["type"] = "full"
113+
record["source"] = "bring-via-sj-mirror"
114+
records.append(record)
115+
116+
print(f"Skipped (regex fail): {skipped_bad_regex:,}")
117+
print(f"Records emitted: {len(records):,}")
118+
119+
if args.dry_run:
120+
return 0
121+
122+
target = project_root / "contributions/postcodes/SJ.json"
123+
target.parent.mkdir(parents=True, exist_ok=True)
124+
if target.exists():
125+
with target.open(encoding="utf-8") as f:
126+
existing = json.load(f)
127+
existing_seen = {
128+
(r["code"], (r.get("locality_name") or "").lower()) for r in existing
129+
}
130+
merged = list(existing)
131+
for r in records:
132+
key = (r["code"], (r.get("locality_name") or "").lower())
133+
if key not in existing_seen:
134+
merged.append(r)
135+
existing_seen.add(key)
136+
merged.sort(key=lambda r: (r["code"], r.get("locality_name", "")))
137+
else:
138+
merged = sorted(records, key=lambda r: (r["code"], r.get("locality_name", "")))
139+
140+
with target.open("w", encoding="utf-8") as f:
141+
json.dump(merged, f, ensure_ascii=False, indent=2)
142+
f.write("\n")
143+
size_kb = target.stat().st_size / 1024
144+
print(
145+
f"\n[OK] Wrote {target.relative_to(project_root)} "
146+
f"({len(merged):,} rows, {size_kb:.0f} KB)"
147+
)
148+
return 0
149+
150+
151+
if __name__ == "__main__":
152+
raise SystemExit(main())

contributions/postcodes/SJ.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
[
2+
{
3+
"code": "8099",
4+
"country_id": 211,
5+
"country_code": "SJ",
6+
"locality_name": "Jan Mayen",
7+
"type": "full",
8+
"source": "bring-via-sj-mirror"
9+
},
10+
{
11+
"code": "9170",
12+
"country_id": 211,
13+
"country_code": "SJ",
14+
"locality_name": "Longyearbyen",
15+
"type": "full",
16+
"source": "bring-via-sj-mirror"
17+
},
18+
{
19+
"code": "9171",
20+
"country_id": 211,
21+
"country_code": "SJ",
22+
"locality_name": "Longyearbyen",
23+
"type": "full",
24+
"source": "bring-via-sj-mirror"
25+
},
26+
{
27+
"code": "9173",
28+
"country_id": 211,
29+
"country_code": "SJ",
30+
"locality_name": "Ny-Ålesund",
31+
"type": "full",
32+
"source": "bring-via-sj-mirror"
33+
},
34+
{
35+
"code": "9174",
36+
"country_id": 211,
37+
"country_code": "SJ",
38+
"locality_name": "Hopen",
39+
"type": "full",
40+
"source": "bring-via-sj-mirror"
41+
},
42+
{
43+
"code": "9175",
44+
"country_id": 211,
45+
"country_code": "SJ",
46+
"locality_name": "Sveagruva",
47+
"type": "full",
48+
"source": "bring-via-sj-mirror"
49+
},
50+
{
51+
"code": "9176",
52+
"country_id": 211,
53+
"country_code": "SJ",
54+
"locality_name": "Bjørnøya",
55+
"type": "full",
56+
"source": "bring-via-sj-mirror"
57+
},
58+
{
59+
"code": "9178",
60+
"country_id": 211,
61+
"country_code": "SJ",
62+
"locality_name": "Barentsburg",
63+
"type": "full",
64+
"source": "bring-via-sj-mirror"
65+
}
66+
]

0 commit comments

Comments
 (0)