#!/usr/bin/env python3 """小平市の境界から500m以内にある近隣市の公共施設をOpenStreetMapから抽出する。 使い方: py -X utf8 scripts/update-nearby-facilities.py 出力: src/data/kodaira-nearby-facilities.json 対象: amenity=library / community_centre / townhall / arts_centre / theatre 小平市内の施設は kodaira-facilities.json 側にあるため除外する。 """ import importlib.util import json import os import urllib.parse import urllib.request ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(ROOT, "src", "data", "kodaira-nearby-facilities.json") MAX_DIST_M = 500.0 BBOX = (35.68, 139.39, 35.76, 139.56) AMENITIES = "library|community_centre|townhall|arts_centre|theatre" EXCLUDE_NAME_PARTS = ("付属図書館", "コミュニティ サロン") ENDPOINTS = [ "https://overpass-api.de/api/interpreter", "https://overpass.kumi.systems/api/interpreter", "https://overpass.private.coffee/api/interpreter", ] def load_module(path, name): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def fetch_overpass(): south, west, north, east = BBOX query = ( "[out:json][timeout:180];" "(" f'node["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' f'way["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' f'relation["amenity"~"{AMENITIES}"]({south},{west},{north},{east});' ");" "out tags center;" ) last_err = None for endpoint in ENDPOINTS: for attempt in range(2): try: req = urllib.request.Request( endpoint, data=query.encode("utf-8"), headers={"User-Agent": "kodaira-nearby-facilities/1.0 (yasutakeyohei.com)"}, ) with urllib.request.urlopen(req, timeout=240) as res: data = json.load(res) if "elements" not in data: raise ValueError("レスポンスに elements がありません") print(f" {endpoint} から {len(data['elements'])} 件取得") return data["elements"] except Exception as exc: # noqa: BLE001 last_err = exc print(f" {endpoint} 失敗 ({attempt + 1}/2): {exc}") raise SystemExit(f"Overpass APIの取得に失敗しました(出力は作成していません): {last_err}") def facility_type(tags): amenity = tags.get("amenity", "") name = tags.get("name", "") if amenity == "library": return "図書館" if amenity == "community_centre": return "児童館" if "児童館" in name else "公民館" if amenity == "townhall": return "市役所支所" if amenity in ("arts_centre", "theatre"): return "文化施設" return "" def address_of(tags): full = (tags.get("addr:full") or "").strip() if full: return full city = (tags.get("addr:city") or "").strip() neighbourhood = ( tags.get("addr:neighbourhood") or tags.get("addr:suburb") or tags.get("addr:quarter") or "" ).strip() street = (tags.get("addr:street") or "").strip() block = (tags.get("addr:block_number") or "").strip() number = (tags.get("addr:housenumber") or "").strip() if block and number: lot = f"{block}-{number}" else: lot = block or number if street: return f"{city}{street}{lot}" if neighbourhood: return f"{city}{neighbourhood}{lot}" # 町名がない番地だけでは住所にならないため、市区町村だけにする。 return city def main(): print("Overpass API から近隣施設を取得中...") elements = fetch_overpass() np = load_module(os.path.join(ROOT, "scripts", "update-nearby-parks.py"), "nearby_parks") boundary_ll = np.load_boundary() lons = [lon for polygon in boundary_ll for ring in polygon for lon, _ in ring] lats = [lat for polygon in boundary_ll for ring in polygon for _, lat in ring] to_xy = np.projector(sum(lats) / len(lats), sum(lons) / len(lons)) boundary_xy = [ [[to_xy(lon, lat) for lon, lat in ring] for ring in polygon] for polygon in boundary_ll ] boundary = np.BoundaryIndex(boundary_xy) features = [] for el in elements: tags = el.get("tags") or {} name = (tags.get("name") or "").strip() if not name: continue if any(part in name for part in EXCLUDE_NAME_PARTS): continue kind = facility_type(tags) if not kind: continue center = el.get("center") or {} lat = el.get("lat", center.get("lat")) lon = el.get("lon", center.get("lon")) if lat is None or lon is None: continue x, y = to_xy(lon, lat) if boundary.contains(x, y): continue distance = boundary.min_distance([(x, y)]) if distance > MAX_DIST_M: continue features.append( { "name": name, "type": kind, "address": address_of(tags), "hours": (tags.get("opening_hours") or "").strip(), "closed": "", "toilet": "未確認", "water": "未確認", "note": f"市外・小平市境から約{int(round(distance))}m(OpenStreetMap)", "source": f"https://www.openstreetmap.org/{el.get('type')}/{el.get('id')}", "lat": lat, "lon": lon, "geocode": "OSM", "city": (tags.get("addr:city") or "").strip(), "distance_m": int(round(distance)), "osm_type": el.get("type"), "osm_id": el.get("id"), "nearby": True, } ) features.sort(key=lambda f: (f["distance_m"], f["name"])) with open(OUT, "w", encoding="utf-8") as fh: json.dump(features, fh, ensure_ascii=False, indent=2) fh.write("\n") print(f"nearby facilities {len(features)} -> {OUT}") if __name__ == "__main__": main()