site-intelligence-studio / src /osm_context.py
Eishaan's picture
Prepare submission-ready Space build
c590d67
Raw
History Blame Contribute Delete
4.96 kB
from __future__ import annotations
import urllib.parse
from collections import Counter
from typing import Any
from .evidence import make_evidence
from .geometry import EARTH_RADIUS_M
from .http_client import post_text
from .models import EvidenceItem, SiteSelection
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
def fetch_osm_context(selection: SiteSelection) -> tuple[dict[str, Any], list[EvidenceItem]]:
if selection.anchor_lat is None or selection.anchor_lon is None:
return {"counts": {}, "features": []}, [
make_evidence(
category="Geographic context",
finding="OSM context was skipped because no real-world anchor coordinate is available.",
source_name="OpenStreetMap",
source_url="https://www.openstreetmap.org/copyright",
source_type="open map data",
resolution_or_scope="not available",
confidence="low",
limitation="Public map data requires a real-world coordinate.",
design_implication="Use CAD/user observations until the site is anchored.",
verification_needed="Provide lat/lon or draw the boundary on the map.",
output_label="site_visit_required",
)
]
radius = int(selection.radius_m or 500)
radius = min(max(radius, 100), 1500)
query = f"""
[out:json][timeout:20];
(
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["highway"];
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["building"];
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["natural"="water"];
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["waterway"];
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["landuse"];
way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["leisure"="park"];
node(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["amenity"];
);
out tags center geom 120;
"""
try:
payload = post_text(OVERPASS_URL, urllib.parse.urlencode({"data": query}))
elements = payload.get("elements") or []
counts = _count_context(elements)
evidence = [
make_evidence(
category="Geographic context",
finding=f"OSM context found {sum(counts.values())} nearby mapped features in the selected search radius.",
source_name="OpenStreetMap / Overpass API",
source_url="https://www.openstreetmap.org/copyright",
source_type="open map data",
resolution_or_scope=f"{radius} m around anchor coordinate",
confidence="medium" if counts else "low",
limitation="OSM tags may be incomplete or outdated; public tile/API use requires attribution.",
design_implication="Use mapped roads, water, green, land-use, and amenities as first-pass context only.",
verification_needed="Verify access, road widths, vegetation, water bodies, and local activity on site.",
output_label="public_data",
)
]
return {"counts": dict(counts), "features": elements[:80], "radius_m": radius}, evidence
except Exception as exc: # noqa: BLE001
return {"counts": {}, "features": [], "radius_m": radius}, [
make_evidence(
category="Geographic context",
finding="OSM context could not be retrieved.",
source_name="OpenStreetMap / Overpass API",
source_url="https://overpass-api.de/",
source_type="open map data",
resolution_or_scope=f"{radius} m around anchor coordinate",
confidence="low",
limitation=f"Overpass request failed: {type(exc).__name__}.",
design_implication="Do not infer mapped context from this missing layer.",
verification_needed="Manually verify roads, water, vegetation, access, and surrounding buildings.",
output_label="site_visit_required",
)
]
def _count_context(elements: list[dict[str, Any]]) -> Counter[str]:
counts: Counter[str] = Counter()
for element in elements:
tags = element.get("tags") or {}
if "highway" in tags:
counts["roads/access"] += 1
if "building" in tags:
counts["buildings"] += 1
if tags.get("natural") == "water" or "waterway" in tags:
counts["water"] += 1
if tags.get("leisure") == "park" or tags.get("landuse") in {"forest", "grass", "recreation_ground"}:
counts["green/open space"] += 1
if "landuse" in tags:
counts[f"landuse:{tags['landuse']}"] += 1
if "amenity" in tags:
counts[f"amenity:{tags['amenity']}"] += 1
return counts