diff options
Diffstat (limited to 'scripts/fill-nearby-cities.py')
| -rw-r--r-- | scripts/fill-nearby-cities.py | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/scripts/fill-nearby-cities.py b/scripts/fill-nearby-cities.py new file mode 100644 index 0000000..9f2a087 --- /dev/null +++ b/scripts/fill-nearby-cities.py @@ -0,0 +1,129 @@ +#!/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() |
