"""PDOK Locatieserver — postcode geocoding.""" from __future__ import annotations import json import re import urllib.parse import urllib.request from typing import Any, Optional PDOK_URL = "https://api.pdok.nl/bzk/locatieserver/search/v3_1/free" POSTCODE_RE = re.compile(r"^(\d{4})\s?([A-Za-z]{2})$") def normalize_postcode(raw: str) -> str: cleaned = (raw or "").strip().upper().replace(" ", "") m = POSTCODE_RE.match(cleaned) if m: return f"{m.group(1)}{m.group(2)}" return cleaned[:6] if cleaned else "" def _parse_point(value: Optional[str]) -> tuple[Optional[float], Optional[float]]: if not value or "POINT" not in value: return None, None nums = re.findall(r"[-+]?\d*\.?\d+", value) if len(nums) >= 2: return float(nums[1]), float(nums[0]) # lat, lon return None, None def lookup_postcode(postcode: str) -> Optional[dict[str, Any]]: pc = normalize_postcode(postcode) if len(pc) < 6: return None q = urllib.parse.urlencode({"q": pc, "rows": 1, "fq": "type:postcode"}) with urllib.request.urlopen(f"{PDOK_URL}?{q}", timeout=20) as resp: data = json.loads(resp.read().decode()) docs = data.get("response", {}).get("docs", []) if not docs: return None doc = docs[0] lat, lon = _parse_point(doc.get("centroide_ll")) gemeente_code = (doc.get("gemeentecode") or "").strip() if gemeente_code and not gemeente_code.startswith("GM"): gemeente_code = f"GM{gemeente_code}" return { "postcode": pc, "city": (doc.get("woonplaatsnaam") or "").strip(), "province": (doc.get("provincienaam") or "").strip(), "province_code": (doc.get("provinciecode") or "").strip(), "municipality": (doc.get("gemeentenaam") or "").strip(), "municipality_code": gemeente_code, "street": (doc.get("straatnaam") or "").strip(), "latitude": lat, "longitude": lon, }