Files
2026-07-03 12:24:58 -04:00

88 lines
2.7 KiB
Python

"""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",
}