"""Consulta la Overpass API para obtener POIs cercanos a una coordenada.""" import json import urllib.error import urllib.parse import urllib.request # Mapeo de categoría legible → (key OSM, value OSM) # None como value significa wildcard (key=*) _CATEGORY_TAGS: dict[str, tuple[str, str | None]] = { "restaurant": ("amenity", "restaurant"), "cafe": ("amenity", "cafe"), "bar": ("amenity", "bar"), "museum": ("tourism", "museum"), "monument": ("historic", "monument"), "park": ("leisure", "park"), "library": ("amenity", "library"), "theatre": ("amenity", "theatre"), "cinema": ("amenity", "cinema"), "gallery": ("tourism", "gallery"), "historic": ("historic", None), "tourism": ("tourism", None), "shop": ("shop", None), "hotel": ("tourism", "hotel"), "pharmacy": ("amenity", "pharmacy"), "hospital": ("amenity", "hospital"), } _DEFAULT_CATEGORIES = [ "restaurant", "cafe", "bar", "museum", "monument", "park", "library", "theatre", "cinema", "gallery", "historic", "tourism", "shop", ] _OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter" def _build_query(lat: float, lon: float, radius_m: int, categories: list[str]) -> str: """Construye una query Overpass QL con union de nodos.""" lines = ["[out:json][timeout:10];", "("] for cat in categories: if cat not in _CATEGORY_TAGS: continue key, value = _CATEGORY_TAGS[cat] if value is None: lines.append(f' node["{key}"](around:{radius_m},{lat},{lon});') else: lines.append(f' node["{key}"="{value}"](around:{radius_m},{lat},{lon});') lines.append(");") lines.append("out body;") return "\n".join(lines) def _extract_category(tags: dict) -> str: """Determina la categoría legible a partir de los tags OSM del nodo.""" for cat, (key, value) in _CATEGORY_TAGS.items(): if key not in tags: continue if value is None or tags[key] == value: return cat return "unknown" def overpass_nearby_pois( lat: float, lon: float, radius_m: int = 500, categories: list[str] | None = None, ) -> list[dict]: """Obtiene POIs cercanos a una coordenada usando la Overpass API. Args: lat: latitud del punto central. lon: longitud del punto central. radius_m: radio de busqueda en metros (defecto 500). categories: lista de categorias a buscar. Si es None, usa todas las categorias comunes definidas internamente. Returns: lista de dicts con id, lat, lon, name, category y tags de cada POI. Raises: RuntimeError: si la request falla por error HTTP, timeout u otro error de red. """ active_categories = categories if categories is not None else _DEFAULT_CATEGORIES query = _build_query(lat, lon, radius_m, active_categories) encoded_query = urllib.parse.quote(query) form_body = f"data={encoded_query}".encode("utf-8") req = urllib.request.Request( _OVERPASS_ENDPOINT, data=form_body, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, ) try: with urllib.request.urlopen(req, timeout=10) as response: raw = response.read() except urllib.error.HTTPError as e: body_preview = e.read(200).decode("utf-8", errors="replace") raise RuntimeError( f"Overpass API HTTP error {e.code} for ({lat},{lon}) r={radius_m}m: {body_preview}" ) from e except urllib.error.URLError as e: raise RuntimeError( f"Overpass API request failed for ({lat},{lon}) r={radius_m}m: {e.reason}" ) from e try: result = json.loads(raw) except json.JSONDecodeError as e: raise RuntimeError( f"Overpass API returned invalid JSON for ({lat},{lon}): {e}" ) from e pois: list[dict] = [] for element in result.get("elements", []): if element.get("type") != "node": continue tags = element.get("tags", {}) category = _extract_category(tags) # Nombre: tag "name", o valor del tag de categoría, o "Unknown" if "name" in tags: name = tags["name"] else: key = _CATEGORY_TAGS.get(category, (None, None))[0] name = tags.get(key, "Unknown") if key else "Unknown" pois.append({ "id": element["id"], "lat": element["lat"], "lon": element["lon"], "name": name, "category": category, "tags": tags, }) return pois