from __future__ import annotations """CLI utility to import station data from OpenStreetMap.""" import argparse import json import sys from dataclasses import asdict from pathlib import Path from typing import Any, Iterable from urllib.parse import quote_plus from backend.app.core.osm_config import ( DEFAULT_REGIONS, STATION_TAG_FILTERS, compile_overpass_filters, ) OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter" def build_argument_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Export OSM station nodes for ingestion" ) parser.add_argument( "--output", type=Path, default=Path("data/osm_stations.json"), help="Destination file for the exported station nodes (default: data/osm_stations.json)", ) parser.add_argument( "--region", choices=[region.name for region in DEFAULT_REGIONS] + ["all"], default="all", help="Region name to export (default: all)", ) parser.add_argument( "--dry-run", action="store_true", help="Do not fetch data; print the Overpass payload only", ) return parser def build_overpass_query(region_name: str) -> str: if region_name == "all": regions = DEFAULT_REGIONS else: regions = tuple( region for region in DEFAULT_REGIONS if region.name == region_name ) if not regions: msg = f"Unknown region {region_name}. Available regions: {[region.name for region in DEFAULT_REGIONS]}" raise ValueError(msg) filters = compile_overpass_filters(STATION_TAG_FILTERS) parts = ["[out:json][timeout:90];", "("] for region in regions: parts.append(f" node{filters}\n ({region.to_overpass_arg()});") parts.append(")") parts.append("; out body; >; out skel qt;") return "\n".join(parts) def perform_request(query: str) -> dict[str, Any]: import urllib.request payload = f"data={quote_plus(query)}".encode("utf-8") request = urllib.request.Request( OVERPASS_ENDPOINT, data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) with urllib.request.urlopen(request, timeout=120) as response: payload = response.read() return json.loads(payload) def normalize_station_elements( elements: Iterable[dict[str, Any]] ) -> list[dict[str, Any]]: """Convert raw Overpass nodes into StationCreate-compatible payloads.""" stations: list[dict[str, Any]] = [] for element in elements: if element.get("type") != "node": continue latitude = element.get("lat") longitude = element.get("lon") if latitude is None or longitude is None: continue tags: dict[str, Any] = element.get("tags", {}) name = tags.get("name") if not name: continue raw_code = tags.get("ref") or tags.get("railway:ref") or tags.get("local_ref") code = str(raw_code) if raw_code is not None else None elevation_tag = tags.get("ele") or tags.get("elevation") try: elevation = float(elevation_tag) if elevation_tag is not None else None except (TypeError, ValueError): elevation = None disused = str(tags.get("disused", "no")).lower() in {"yes", "true"} railway_status = str(tags.get("railway", "")).lower() abandoned = railway_status in {"abandoned", "disused"} is_active = not (disused or abandoned) stations.append( { "osm_id": str(element.get("id")), "name": str(name), "latitude": float(latitude), "longitude": float(longitude), "code": code, "elevation_m": elevation, "is_active": is_active, } ) return stations def main(argv: list[str] | None = None) -> int: parser = build_argument_parser() args = parser.parse_args(argv) query = build_overpass_query(args.region) if args.dry_run: print(query) return 0 output_path: Path = args.output output_path.parent.mkdir(parents=True, exist_ok=True) data = perform_request(query) raw_elements = data.get("elements", []) stations = normalize_station_elements(raw_elements) payload = { "metadata": { "endpoint": OVERPASS_ENDPOINT, "region": args.region, "filters": STATION_TAG_FILTERS, "regions": [asdict(region) for region in DEFAULT_REGIONS], "raw_count": len(raw_elements), "station_count": len(stations), }, "stations": stations, } with output_path.open("w", encoding="utf-8") as handle: json.dump(payload, handle, indent=2) print( f"Normalized {len(stations)} stations from {len(raw_elements)} elements into {output_path}" ) return 0 if __name__ == "__main__": sys.exit(main())