#!/usr/bin/env python3 """周辺公園・周辺施設の city が空のとき、Nominatimで市区町村を補完する。 使い方: py -X utf8 scripts/fill-nearby-cities.py - src/data/kodaira-nearby-parks.json の properties.city - src/data/kodaira-nearby-facilities.json の city 既に city が入っている行は変更しない。Nominatimの利用ポリシーに従い、 逆ジオコーディングは1リクエストあたり約1.1秒待つ。 """ import json import os import re import time import urllib.parse import urllib.request ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PARKS = os.path.join(ROOT, "src", "data", "kodaira-nearby-parks.json") FACILITIES = os.path.join(ROOT, "src", "data", "kodaira-nearby-facilities.json") UA = "yasutakeyohei.com nearby city fill (https://yasutakeyohei.com/)" def centroid(feature): geom = feature.get("geometry") or {} if geom.get("type") == "Point": lon, lat = geom["coordinates"][:2] return lat, lon if geom.get("type") == "Polygon": ring = geom.get("coordinates", [[]])[0] elif geom.get("type") == "MultiPolygon": ring = geom.get("coordinates", [[[]]])[0][0] else: return None, None if not ring: return None, None lon = sum(p[0] for p in ring) / len(ring) lat = sum(p[1] for p in ring) / len(ring) return lat, lon def city_from_text(text): text = text or "" # 「東村山市」のように市区町村名の中に「村」が含まれるため、市→町→村の順で探す。 for suffix in ("市", "町", "村"): match = re.search(r"(?:東京都)?([^\s0-9]+?" + suffix + r")", text) if match: return match.group(1) return "" def reverse_city(lat, lon): params = urllib.parse.urlencode( { "format": "jsonv2", "lat": lat, "lon": lon, "zoom": 10, "addressdetails": 1, "accept-language": "ja", } ) url = "https://nominatim.openstreetmap.org/reverse?" + params req = urllib.request.Request(url, headers={"User-Agent": UA}) try: with urllib.request.urlopen(req, timeout=30) as res: data = json.load(res) except Exception as exc: # noqa: BLE001 print(f" warning: reverse failed ({lat},{lon}): {exc}") return "" finally: time.sleep(1.1) address = data.get("address") or {} for key in ("city", "town", "village", "municipality"): value = (address.get(key) or "").strip() if value: return value return "" def main(): parks_data = json.load(open(PARKS, encoding="utf-8")) parks_changed = 0 for feature in parks_data.get("features", []): props = feature.setdefault("properties", {}) if (props.get("city") or "").strip(): continue lat, lon = centroid(feature) if lat is None or lon is None: continue city = reverse_city(lat, lon) if city: props["city"] = city parks_changed += 1 print(f" park: {props.get('name')} -> {city}") if parks_changed: with open(PARKS, "w", encoding="utf-8") as fh: json.dump(parks_data, fh, ensure_ascii=False, separators=(",", ":")) fh.write("\n") facilities = json.load(open(FACILITIES, encoding="utf-8")) facilities_changed = 0 for item in facilities: if (item.get("city") or "").strip(): continue city = city_from_text(item.get("address", "")) or city_from_text(item.get("name", "")) if not city: lat = item.get("lat") lon = item.get("lon") if lat is not None and lon is not None: city = reverse_city(lat, lon) if city: item["city"] = city facilities_changed += 1 print(f" facility: {item.get('name')} -> {city}") if facilities_changed: with open(FACILITIES, "w", encoding="utf-8") as fh: json.dump(facilities, fh, ensure_ascii=False, indent=2) fh.write("\n") print(f"updated parks {parks_changed}, facilities {facilities_changed}") if __name__ == "__main__": main()