"""FEMA NFHL flood zone lookup por lat/lng. API publica: https://hazards.fema.gov/gis/nfhl/rest/services/public/NFHL/MapServer Layer 28 = "S_FLD_HAZ_AR" (Special Flood Hazard Areas). Sin key requerida. Sin rate limits estrictos. Devuelve dict con: zone: "X" / "X (shaded)" / "A" / "AE" / "AH" / "AO" / "V" / "VE" / etc. bfe: Base Flood Elevation (ft) o None sfha: bool - True si esta en Special Flood Hazard Area subtype: subzone description o None """ from __future__ import annotations import requests from .base import FetcherError, DEFAULT_TIMEOUT FEMA_URL = "https://hazards.fema.gov/arcgis/rest/services/public/NFHL/MapServer/28/query" # Zonas que son SFHA (Special Flood Hazard Area) segun FEMA SFHA_ZONES = {"A", "AE", "AH", "AO", "AR", "A99", "V", "VE", "VO"} def fetch_flood(lat: float, lng: float) -> dict: """Consulta FEMA NFHL para flood zone en (lat, lng). Si el punto NO esta en ninguna SFHA, FEMA devuelve features vacio y se interpreta como zona X (low risk, default outside SFHA). """ if lat is None or lng is None: raise FetcherError("lat/lng requeridos") params = { "geometry": f"{lng},{lat}", # FEMA usa lng,lat (x,y) "geometryType": "esriGeometryPoint", "inSR": "4326", # WGS84 "spatialRel": "esriSpatialRelIntersects", "outFields": "FLD_ZONE,STATIC_BFE,ZONE_SUBTY", "returnGeometry": "false", "f": "json", } try: r = requests.get(FEMA_URL, params=params, timeout=DEFAULT_TIMEOUT) r.raise_for_status() except requests.RequestException as e: raise FetcherError(f"HTTP error: {e}") from e try: data = r.json() except ValueError as e: raise FetcherError(f"JSON parse error: {e}") from e # FEMA puede devolver "error" si la query es invalida if "error" in data: raise FetcherError(f"FEMA API error: {data['error']}") features = data.get("features", []) if not features: # Punto fuera de SFHA → low-risk zone X return { "zone": "X", "bfe": None, "sfha": False, "subtype": None, "source": "FEMA NFHL (outside SFHA)", } attrs = features[0].get("attributes", {}) or {} zone = (attrs.get("FLD_ZONE") or "unknown").strip() subtype = attrs.get("ZONE_SUBTY") # BFE: FEMA usa -9999 para "no aplica" bfe_raw = attrs.get("STATIC_BFE") bfe = bfe_raw if (bfe_raw is not None and bfe_raw != -9999) else None return { "zone": zone, "bfe": bfe, "sfha": zone in SFHA_ZONES, "subtype": subtype, "source": "FEMA NFHL", }