v0.14: regional_risk_agent — FEMA NRI multi-hazard at county level
Browse filesBroadens FlutIQ from flood-only to multi-hazard. Every county in the US
now gets a calibrated risk profile across 18 hazards (wildfire, hurricane,
tornado, earthquake, drought, heat wave, etc.) at the neighborhood level
from FEMA's National Risk Index — the canonical source.
The win that matters: NRI's data was previously gated behind FEMA's
click-through CSV download. Found the exposed ArcGIS REST endpoint
behind RAPT (Resilience Analysis and Planning Tool):
https://services.arcgis.com/XG15cJAlne2vxtgt/arcgis/rest/services/
National_Risk_Index_Counties/FeatureServer/0/query
Free, no auth, point queries, 467 fields per county, full national
coverage. Verified live across 6 geographies with very different
hazard fingerprints:
Chicago: Cold wave + Winter weather + Tornado + Inland flood
LA: Earthquake + Wildfire + Inland flood (Community
Resilience: Very Low — interesting demo signal)
Houston: Hurricane + Tornado + Inland flood + Lightning
Miami: Hurricane + Coastal flood + Lightning
Boulder: Wildfire + Lightning + Hail (Front Range fingerprint)
Anchorage: Avalanche + Earthquake + Volcanic activity
Gemma 4 ran on Chicago and produced authentically Chicago-specific
headline-hazard explanations:
'extreme Great Lakes winter cycles' (Winter Weather)
'Midwest corridor susceptibility' (Tornadoes)
'Urban heat island effect in Chicago' (Heat Waves)
Backend:
+ backend/app/tools/nri_county.py — Free ArcGIS point query against
FEMA's NRI Counties FeatureServer. Curates the 467-field response
down to: composite risk score+rating, top 5 hazards by NRI rating
tier, social vulnerability, community resilience, total EAL.
Field-name nuance documented (NRI's _RISKR is rating string,
_RISKS is numeric score — opposite of how the schema aliases
read).
+ backend/app/agents/regional_risk_agent.py — wraps the tool, asks
Gemma 4 to:
1. Pick 2-4 hazards that GENUINELY matter for THIS county
(not just 'all are very high' — that's a population-scale
artifact for big counties)
2. Note what the county-level NRI flood score adds vs the
property-level FEMA zone
3. Interpret SoVI + Community Resilience as recovery context
+ orchestrator: 'regional' added to the parallel data-agent pack.
~ risk_agent: prompt now includes a 'Regional Multi-Hazard Profile'
section so the synthesis can cross-reference county-level NRI with
property-level FEMA / 311 / satellite findings.
Frontend:
+ AGENTS list gets 'Regional risk analyst' (and 'Satellite surveyor'
which was previously missing from the agents-screen even though
it ran).
+ New §05 dossier section 'Wider neighborhood — beyond just flooding'
— purple-accented, only renders when NRI data is available. Shows
the composite badge, EAL, SoVI/Resilience chips, top hazards as a
numbered list with severity colors, plus Gemma's flood-misses
callout and resilience-context callout, sourced from RAPT.
+ Section numbering refactored to a useMemo helper (_sectionNums)
that walks through conditional sections counting up — cleaner
than the nested-conditional approach we had.
Version: chrome wordmark v0.13 → v0.14, app version 0.13.0 → 0.14.0.
- app/agents/orchestrator.py +14 -2
- app/agents/regional_risk_agent.py +130 -0
- app/agents/risk_agent.py +9 -0
- app/main.py +1 -1
- app/tools/nri_county.py +201 -0
- static/index.html +110 -5
|
@@ -21,6 +21,7 @@ from app.agents.archive_agent import run_archive_agent
|
|
| 21 |
from app.agents.fema_agent import run_fema_agent
|
| 22 |
from app.agents.local_agent import run_local_agent
|
| 23 |
from app.agents.news_agent import run_news_agent
|
|
|
|
| 24 |
from app.agents.risk_agent import run_risk_agent
|
| 25 |
from app.agents.satellite_agent import run_satellite_agent
|
| 26 |
from app.agents.streetview_agent import run_streetview_agent
|
|
@@ -78,10 +79,19 @@ def _make_satellite(language: str) -> AgentFn:
|
|
| 78 |
return _run
|
| 79 |
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
# Order here is the order the frontend renders agent rows in.
|
| 82 |
# The two vision agents (streetview eye-level + satellite bird's-eye)
|
| 83 |
-
# are placed last
|
| 84 |
-
#
|
| 85 |
def _data_agents_for(language: str) -> dict[str, AgentFn]:
|
| 86 |
return {
|
| 87 |
"fema": _fema,
|
|
@@ -89,6 +99,7 @@ def _data_agents_for(language: str) -> dict[str, AgentFn]:
|
|
| 89 |
"weather": _weather,
|
| 90 |
"news": _news,
|
| 91 |
"archive": _archive,
|
|
|
|
| 92 |
"satellite": _make_satellite(language),
|
| 93 |
"streetview": _make_streetview(language),
|
| 94 |
}
|
|
@@ -260,6 +271,7 @@ def _compile_dossier(geo: GeoCtx, results: dict) -> dict:
|
|
| 260 |
"archive": results.get("archive", {}),
|
| 261 |
"streetview": results.get("streetview", {}),
|
| 262 |
"satellite": results.get("satellite", {}),
|
|
|
|
| 263 |
"risk": results.get("risk", {}),
|
| 264 |
"advisor": results.get("advisor", {}),
|
| 265 |
}
|
|
|
|
| 21 |
from app.agents.fema_agent import run_fema_agent
|
| 22 |
from app.agents.local_agent import run_local_agent
|
| 23 |
from app.agents.news_agent import run_news_agent
|
| 24 |
+
from app.agents.regional_risk_agent import run_regional_risk_agent
|
| 25 |
from app.agents.risk_agent import run_risk_agent
|
| 26 |
from app.agents.satellite_agent import run_satellite_agent
|
| 27 |
from app.agents.streetview_agent import run_streetview_agent
|
|
|
|
| 79 |
return _run
|
| 80 |
|
| 81 |
|
| 82 |
+
def _make_regional(language: str) -> AgentFn:
|
| 83 |
+
async def _run(ctx: GeoCtx) -> dict:
|
| 84 |
+
return await run_regional_risk_agent(
|
| 85 |
+
ctx["lat"], ctx["lon"], ctx.get("display_name", ""),
|
| 86 |
+
language=language,
|
| 87 |
+
)
|
| 88 |
+
return _run
|
| 89 |
+
|
| 90 |
+
|
| 91 |
# Order here is the order the frontend renders agent rows in.
|
| 92 |
# The two vision agents (streetview eye-level + satellite bird's-eye)
|
| 93 |
+
# are placed last so users see the slower vision calls finishing after
|
| 94 |
+
# the cheap text-API calls.
|
| 95 |
def _data_agents_for(language: str) -> dict[str, AgentFn]:
|
| 96 |
return {
|
| 97 |
"fema": _fema,
|
|
|
|
| 99 |
"weather": _weather,
|
| 100 |
"news": _news,
|
| 101 |
"archive": _archive,
|
| 102 |
+
"regional": _make_regional(language),
|
| 103 |
"satellite": _make_satellite(language),
|
| 104 |
"streetview": _make_streetview(language),
|
| 105 |
}
|
|
|
|
| 271 |
"archive": results.get("archive", {}),
|
| 272 |
"streetview": results.get("streetview", {}),
|
| 273 |
"satellite": results.get("satellite", {}),
|
| 274 |
+
"regional": results.get("regional", {}),
|
| 275 |
"risk": results.get("risk", {}),
|
| 276 |
"advisor": results.get("advisor", {}),
|
| 277 |
}
|
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Regional risk agent — area-level multi-hazard context.
|
| 3 |
+
|
| 4 |
+
Wraps the FEMA NRI county lookup. Asks Gemma 4 to:
|
| 5 |
+
1. Identify which 2-3 hazards genuinely matter for THIS county
|
| 6 |
+
(not just "all are very high" — that's true for any big-population
|
| 7 |
+
county on a national-percentile-ranked index)
|
| 8 |
+
2. Frame the result against the property-level flood signals the
|
| 9 |
+
other agents already produced
|
| 10 |
+
3. Note social vulnerability + community resilience as risk-context
|
| 11 |
+
modulators, not extra hazards
|
| 12 |
+
|
| 13 |
+
The point of this agent is to broaden FlutIQ from flood-only to
|
| 14 |
+
"here's the multi-hazard picture of your neighborhood." Wildfire,
|
| 15 |
+
hurricane, earthquake, tornado are all surfaced in plain English at
|
| 16 |
+
the COUNTY (not address) level — exactly the resolution of FEMA's NRI.
|
| 17 |
+
"""
|
| 18 |
+
import json
|
| 19 |
+
|
| 20 |
+
from app.data.languages import prompt_directive
|
| 21 |
+
from app.llm.client import call_gemma4, extract_text, parse_json_response
|
| 22 |
+
from app.tools.nri_county import lookup_nri_county
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
SYSTEM_PROMPT = """You are a regional natural-hazard analyst. You read FEMA's National Risk Index (NRI) county-level data and explain in plain language which hazards meaningfully affect a specific neighborhood.
|
| 26 |
+
|
| 27 |
+
Critical guidance:
|
| 28 |
+
- NRI scores are NATIONAL PERCENTILES. A county at the 99th percentile for tornado risk is in the top 1% of the country for tornado risk — that's meaningful. But large-population counties tend to score Very High on many hazards just because more people + property = more expected loss; rating alone is not enough, look at the SCORES too.
|
| 29 |
+
- Distinguish "this county is genuinely high-risk for hazard X" (high score AND meaningful expected annual loss vs population) from "this county hits Very High because of population scale, not unusual hazard exposure."
|
| 30 |
+
- Cross-reference with property-level flood signals provided by other agents. NRI's Inland Flooding score is COUNTY-LEVEL; the property's actual FEMA Zone designation and 311 record are more precise.
|
| 31 |
+
- Social Vulnerability and Community Resilience are POPULATION-level — they describe how well the surrounding community can respond to and recover from a disaster. Note them; don't conflate with hazard exposure.
|
| 32 |
+
- Plain English. Avoid hazard-jargon (e.g. say "tornadoes" not "convective windstorm events").
|
| 33 |
+
|
| 34 |
+
Always respond with valid JSON only."""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
async def run_regional_risk_agent(
|
| 38 |
+
lat: float,
|
| 39 |
+
lon: float,
|
| 40 |
+
address: str,
|
| 41 |
+
fema_findings: dict | None = None,
|
| 42 |
+
language: str = "en",
|
| 43 |
+
) -> dict:
|
| 44 |
+
nri = await lookup_nri_county(lat, lon)
|
| 45 |
+
|
| 46 |
+
if not nri.get("available"):
|
| 47 |
+
return {
|
| 48 |
+
"available": False,
|
| 49 |
+
"summary": (
|
| 50 |
+
f"FEMA National Risk Index lookup unavailable: "
|
| 51 |
+
f"{nri.get('error', 'unknown reason')}"
|
| 52 |
+
),
|
| 53 |
+
"raw": nri,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# Trim the raw NRI for the prompt — keep top hazards + ratings only,
|
| 57 |
+
# drop the long all-18-hazards array since the model already gets
|
| 58 |
+
# the top 5 ranked.
|
| 59 |
+
nri_for_prompt = {
|
| 60 |
+
"county": nri["county"],
|
| 61 |
+
"state": nri["state"],
|
| 62 |
+
"population": nri.get("population"),
|
| 63 |
+
"composite_risk_score": nri["composite_risk_score"],
|
| 64 |
+
"composite_risk_rating": nri["composite_risk_rating"],
|
| 65 |
+
"expected_annual_loss_usd": nri["expected_annual_loss_usd"],
|
| 66 |
+
"social_vulnerability_rating": nri["social_vulnerability_rating"],
|
| 67 |
+
"community_resilience_rating": nri["community_resilience_rating"],
|
| 68 |
+
"top_hazards": nri["top_hazards"],
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
fema_for_prompt = (
|
| 72 |
+
{k: v for k, v in (fema_findings or {}).items() if k != "raw"}
|
| 73 |
+
if fema_findings else None
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
user_prompt = f"""Analyze the FEMA National Risk Index profile for the property's COUNTY.
|
| 77 |
+
|
| 78 |
+
Address: {address}
|
| 79 |
+
|
| 80 |
+
## NRI county-level signal
|
| 81 |
+
{json.dumps(nri_for_prompt, indent=2, default=str)}
|
| 82 |
+
|
| 83 |
+
## Property-level FEMA flood designation (from the property-specific FEMA agent)
|
| 84 |
+
{json.dumps(fema_for_prompt, indent=2, default=str) if fema_for_prompt else "(not available)"}
|
| 85 |
+
|
| 86 |
+
Return a JSON object:
|
| 87 |
+
|
| 88 |
+
{{
|
| 89 |
+
"headline_hazards": [
|
| 90 |
+
{{
|
| 91 |
+
"name": "<short hazard name, e.g. 'Hurricane', 'Wildfire'>",
|
| 92 |
+
"rating": "<copy from top_hazards above>",
|
| 93 |
+
"score": <copy from top_hazards above>,
|
| 94 |
+
"matters_because": "<1 sentence explaining why this hazard genuinely affects THIS county — geography, climate, history. Do not just restate the rating.>"
|
| 95 |
+
}}
|
| 96 |
+
],
|
| 97 |
+
"what_property_level_flood_misses": "<1-2 sentences on what the county-level NRI flood score adds vs. the property's specific FEMA Zone designation. If the FEMA agent already showed this is a SFHA property, say so. If FEMA says minimal but NRI inland-flooding is Very High, that's a meaningful gap to call out.>",
|
| 98 |
+
"resilience_context": "<1-2 sentences interpreting Social Vulnerability + Community Resilience for this area, in plain English. Examples: 'Highly resilient community: well-resourced response infrastructure.' / 'Lower community resilience means a similar storm here would take longer to recover from than in a higher-resilience county.'>",
|
| 99 |
+
"summary": "<1 sentence for the status feed>"
|
| 100 |
+
}}
|
| 101 |
+
|
| 102 |
+
CONSTRAINTS:
|
| 103 |
+
- headline_hazards: pick 2 to 4 hazards. Skip anything with rating "No Rating" / "Insufficient Data" / "Not Applicable".
|
| 104 |
+
- Don't pad with non-meaningful hazards just to fill 4 slots.
|
| 105 |
+
- If the county is genuinely low-risk overall (composite < 50), say so honestly.
|
| 106 |
+
- Return ONLY the JSON object."""
|
| 107 |
+
|
| 108 |
+
response = await call_gemma4(
|
| 109 |
+
messages=[
|
| 110 |
+
{"role": "system", "content": SYSTEM_PROMPT + prompt_directive(language)},
|
| 111 |
+
{"role": "user", "content": user_prompt},
|
| 112 |
+
],
|
| 113 |
+
temperature=0.2,
|
| 114 |
+
max_tokens=2000,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
text = extract_text(response)
|
| 118 |
+
parsed = parse_json_response(text) or {}
|
| 119 |
+
|
| 120 |
+
# Always pass through the structured NRI data so the dossier UI can
|
| 121 |
+
# render the full top-hazards table even if the model's interpretation
|
| 122 |
+
# truncated to 2.
|
| 123 |
+
parsed["available"] = True
|
| 124 |
+
parsed["nri"] = nri
|
| 125 |
+
if "summary" not in parsed:
|
| 126 |
+
parsed["summary"] = (
|
| 127 |
+
f"County composite risk: {nri['composite_risk_rating']} "
|
| 128 |
+
f"({nri['composite_risk_score']}/100)"
|
| 129 |
+
)
|
| 130 |
+
return parsed
|
|
@@ -110,6 +110,15 @@ Here is all the data collected by our investigation team:
|
|
| 110 |
## Satellite Visual Analysis (from the satellite agent)
|
| 111 |
{json.dumps({k: v for k, v in (all_data.get('satellite') or {}).items() if k != 'image_data_url'}, indent=2, default=str)}
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
## Weather & Hydrology Findings
|
| 114 |
{json.dumps(all_data.get('weather', {}), indent=2, default=str)}
|
| 115 |
|
|
|
|
| 110 |
## Satellite Visual Analysis (from the satellite agent)
|
| 111 |
{json.dumps({k: v for k, v in (all_data.get('satellite') or {}).items() if k != 'image_data_url'}, indent=2, default=str)}
|
| 112 |
|
| 113 |
+
## Regional Multi-Hazard Profile (FEMA NRI, county-level — from the regional_risk agent)
|
| 114 |
+
This is the COUNTY-level National Risk Index profile — wider than the
|
| 115 |
+
property-specific signals above, narrower than national. Use it to widen
|
| 116 |
+
the risk story beyond flooding (wildfire, hurricane, tornado, earthquake,
|
| 117 |
+
etc. as applicable to this geography). Don't double-count: NRI's Inland
|
| 118 |
+
Flooding score is county-level; the FEMA flood zone above is the
|
| 119 |
+
authoritative property-level designation.
|
| 120 |
+
{json.dumps({k: v for k, v in (all_data.get('regional') or {}).items() if k != 'nri'}, indent=2, default=str)}
|
| 121 |
+
|
| 122 |
## Weather & Hydrology Findings
|
| 123 |
{json.dumps(all_data.get('weather', {}), indent=2, default=str)}
|
| 124 |
|
|
@@ -8,7 +8,7 @@ from fastapi.staticfiles import StaticFiles
|
|
| 8 |
from app.api.assess import router as assess_router
|
| 9 |
from app.api.health import router as health_router
|
| 10 |
|
| 11 |
-
app = FastAPI(title="FlutIQ", version="0.
|
| 12 |
|
| 13 |
# CORS still permissive for split-deployment scenarios. With the
|
| 14 |
# bundled deploy (frontend served from FastAPI) it's a no-op because
|
|
|
|
| 8 |
from app.api.assess import router as assess_router
|
| 9 |
from app.api.health import router as health_router
|
| 10 |
|
| 11 |
+
app = FastAPI(title="FlutIQ", version="0.14.0")
|
| 12 |
|
| 13 |
# CORS still permissive for split-deployment scenarios. With the
|
| 14 |
# bundled deploy (frontend served from FastAPI) it's a no-op because
|
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FEMA National Risk Index — county-level multi-hazard scores.
|
| 3 |
+
|
| 4 |
+
Why this matters: every other agent in FlutIQ is flood-focused. NRI gives
|
| 5 |
+
us the calibrated, neighborhood-level "everything else" — wildfire,
|
| 6 |
+
hurricane, tornado, earthquake, drought, heat wave, lightning, etc., plus
|
| 7 |
+
Social Vulnerability and Community Resilience indices.
|
| 8 |
+
|
| 9 |
+
Source: FEMA's Resilience Analysis and Planning Tool (RAPT) hosts the
|
| 10 |
+
NRI Counties dataset on its public ArcGIS Online org. The FeatureServer
|
| 11 |
+
accepts point queries without auth — exactly what we need.
|
| 12 |
+
|
| 13 |
+
https://services.arcgis.com/XG15cJAlne2vxtgt/arcgis/rest/services/
|
| 14 |
+
National_Risk_Index_Counties/FeatureServer/0/query?...
|
| 15 |
+
|
| 16 |
+
Coverage: all 3,143 US counties + county-equivalents. Updated annually.
|
| 17 |
+
|
| 18 |
+
Each county record has 467 fields. We surface a curated subset:
|
| 19 |
+
- Composite risk score + rating + percentile
|
| 20 |
+
- Per-hazard score + rating for the 18 NRI hazards
|
| 21 |
+
- Social Vulnerability + Community Resilience
|
| 22 |
+
- Total expected annual loss in dollars
|
| 23 |
+
|
| 24 |
+
We DROP all the per-asset breakdown fields (building / population /
|
| 25 |
+
agriculture EAL split out across 18 hazards = 100+ columns) because
|
| 26 |
+
the dossier doesn't need them and they bloat the prompt budget.
|
| 27 |
+
"""
|
| 28 |
+
import json
|
| 29 |
+
|
| 30 |
+
import httpx
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
SERVICE_URL = (
|
| 34 |
+
"https://services.arcgis.com/XG15cJAlne2vxtgt/arcgis/rest/services/"
|
| 35 |
+
"National_Risk_Index_Counties/FeatureServer/0/query"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# 18 NRI hazards — codes used in field names + display labels.
|
| 39 |
+
# Order matters: most common-to-explain hazards first so any "top N"
|
| 40 |
+
# truncation surfaces the most-recognizable ones.
|
| 41 |
+
HAZARDS: list[tuple[str, str]] = [
|
| 42 |
+
("IFLD", "Inland flooding"),
|
| 43 |
+
("CFLD", "Coastal flooding"),
|
| 44 |
+
("HRCN", "Hurricane"),
|
| 45 |
+
("WFIR", "Wildfire"),
|
| 46 |
+
("ERQK", "Earthquake"),
|
| 47 |
+
("TRND", "Tornado"),
|
| 48 |
+
("HAIL", "Hail"),
|
| 49 |
+
("HWAV", "Heat wave"),
|
| 50 |
+
("CWAV", "Cold wave"),
|
| 51 |
+
("DRGT", "Drought"),
|
| 52 |
+
("WNTW", "Winter weather"),
|
| 53 |
+
("ISTM", "Ice storm"),
|
| 54 |
+
("SWND", "Strong wind"),
|
| 55 |
+
("LTNG", "Lightning"),
|
| 56 |
+
("LNDS", "Landslide"),
|
| 57 |
+
("TSUN", "Tsunami"),
|
| 58 |
+
("VLCN", "Volcanic activity"),
|
| 59 |
+
("AVLN", "Avalanche"),
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
# Order from FEMA's NRI rating bins, low → high. Used to bucket-rank.
|
| 63 |
+
_RATING_ORDER = (
|
| 64 |
+
"Very Low",
|
| 65 |
+
"Relatively Low",
|
| 66 |
+
"Relatively Moderate",
|
| 67 |
+
"Relatively High",
|
| 68 |
+
"Very High",
|
| 69 |
+
"No Rating",
|
| 70 |
+
"Insufficient Data",
|
| 71 |
+
"Not Applicable",
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _is_meaningful_rating(s: str | None) -> bool:
|
| 76 |
+
if not s:
|
| 77 |
+
return False
|
| 78 |
+
s = s.strip()
|
| 79 |
+
return s not in ("No Rating", "Insufficient Data", "Not Applicable", "")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _rating_rank(s: str | None) -> int:
|
| 83 |
+
"""Rank a rating from 0 (very low) to 4 (very high). Non-rated → -1."""
|
| 84 |
+
try:
|
| 85 |
+
idx = _RATING_ORDER.index((s or "").strip())
|
| 86 |
+
return idx if idx <= 4 else -1
|
| 87 |
+
except ValueError:
|
| 88 |
+
return -1
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
async def lookup_nri_county(lat: float, lon: float) -> dict:
|
| 92 |
+
"""Point query the NRI Counties FeatureServer for (lat, lon).
|
| 93 |
+
|
| 94 |
+
Returns a dict with:
|
| 95 |
+
- county / state / fips
|
| 96 |
+
- composite risk: score (0-100) + rating
|
| 97 |
+
- expected_annual_loss_usd
|
| 98 |
+
- hazards: list of {code, name, score, rating, rank} sorted by rank desc
|
| 99 |
+
- top_hazards: top 5 by rating rank (only meaningfully-rated ones)
|
| 100 |
+
- social_vulnerability: rating + score
|
| 101 |
+
- community_resilience: rating + score
|
| 102 |
+
"""
|
| 103 |
+
params = {
|
| 104 |
+
"geometry": json.dumps({
|
| 105 |
+
"x": lon, "y": lat,
|
| 106 |
+
"spatialReference": {"wkid": 4326},
|
| 107 |
+
}),
|
| 108 |
+
"geometryType": "esriGeometryPoint",
|
| 109 |
+
"inSR": "4326",
|
| 110 |
+
"outFields": "*",
|
| 111 |
+
"returnGeometry": "false",
|
| 112 |
+
"f": "json",
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async with httpx.AsyncClient(timeout=20, follow_redirects=True) as client:
|
| 116 |
+
resp = await client.get(SERVICE_URL, params=params)
|
| 117 |
+
|
| 118 |
+
if resp.status_code != 200:
|
| 119 |
+
return {"available": False, "error": f"HTTP {resp.status_code}"}
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
data = resp.json()
|
| 123 |
+
except ValueError:
|
| 124 |
+
return {"available": False, "error": "non-JSON response"}
|
| 125 |
+
|
| 126 |
+
if isinstance(data, dict) and "error" in data:
|
| 127 |
+
return {
|
| 128 |
+
"available": False,
|
| 129 |
+
"error": (data["error"].get("message") or "ArcGIS error"),
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
features = data.get("features") or []
|
| 133 |
+
if not features:
|
| 134 |
+
return {
|
| 135 |
+
"available": False,
|
| 136 |
+
"error": "No NRI county polygon at this point (out of US?)",
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
a = features[0].get("attributes") or {}
|
| 140 |
+
|
| 141 |
+
hazards = []
|
| 142 |
+
for code, name in HAZARDS:
|
| 143 |
+
# NRI field naming, verified against live response 2026-05-06:
|
| 144 |
+
# _RISKR = Rating (string like "Very High")
|
| 145 |
+
# _RISKS = Score (numeric 0-100)
|
| 146 |
+
# _RISKV = Value (dollar expected loss) — not used here
|
| 147 |
+
rating = a.get(f"{code}_RISKR")
|
| 148 |
+
score = a.get(f"{code}_RISKS")
|
| 149 |
+
if score is None and not _is_meaningful_rating(rating):
|
| 150 |
+
continue
|
| 151 |
+
hazards.append({
|
| 152 |
+
"code": code,
|
| 153 |
+
"name": name,
|
| 154 |
+
"score": (
|
| 155 |
+
round(float(score), 2)
|
| 156 |
+
if isinstance(score, (int, float))
|
| 157 |
+
else None
|
| 158 |
+
),
|
| 159 |
+
"rating": rating if _is_meaningful_rating(rating) else None,
|
| 160 |
+
"rank": _rating_rank(rating),
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
hazards_meaningful = [h for h in hazards if h["rank"] >= 0]
|
| 164 |
+
hazards_meaningful.sort(
|
| 165 |
+
key=lambda h: (-h["rank"], -(h["score"] or 0)),
|
| 166 |
+
)
|
| 167 |
+
top_hazards = hazards_meaningful[:5]
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"available": True,
|
| 171 |
+
"county": a.get("COUNTY"),
|
| 172 |
+
"state": a.get("STATEABBRV"),
|
| 173 |
+
"state_full": a.get("STATE"),
|
| 174 |
+
"fips": a.get("STCOFIPS"),
|
| 175 |
+
"population": a.get("POPULATION"),
|
| 176 |
+
"composite_risk_score": (
|
| 177 |
+
round(float(a["RISK_SCORE"]), 2)
|
| 178 |
+
if isinstance(a.get("RISK_SCORE"), (int, float)) else None
|
| 179 |
+
),
|
| 180 |
+
"composite_risk_rating": a.get("RISK_RATNG"),
|
| 181 |
+
"expected_annual_loss_usd": (
|
| 182 |
+
int(a["EAL_VALT"])
|
| 183 |
+
if isinstance(a.get("EAL_VALT"), (int, float)) else None
|
| 184 |
+
),
|
| 185 |
+
"social_vulnerability_rating": a.get("SOVI_RATNG"),
|
| 186 |
+
"social_vulnerability_score": (
|
| 187 |
+
round(float(a["SOVI_SCORE"]), 2)
|
| 188 |
+
if isinstance(a.get("SOVI_SCORE"), (int, float)) else None
|
| 189 |
+
),
|
| 190 |
+
"community_resilience_rating": a.get("RESL_RATNG"),
|
| 191 |
+
"community_resilience_score": (
|
| 192 |
+
round(float(a["RESL_SCORE"]), 2)
|
| 193 |
+
if isinstance(a.get("RESL_SCORE"), (int, float)) else None
|
| 194 |
+
),
|
| 195 |
+
"hazards": hazards_meaningful,
|
| 196 |
+
"top_hazards": top_hazards,
|
| 197 |
+
"source": (
|
| 198 |
+
"FEMA National Risk Index, county level. Hosted via FEMA's "
|
| 199 |
+
"Resilience Analysis and Planning Tool (RAPT)."
|
| 200 |
+
),
|
| 201 |
+
}
|
|
@@ -540,6 +540,12 @@ const AGENTS = [
|
|
| 540 |
{ id: "archive", name: "Archivist",
|
| 541 |
finding: "Cook County: 11 NOAA flood events in 10yr, 4 FEMA disaster declarations. 2008, 2013, 2020, 2023 caused widespread basement flooding.",
|
| 542 |
pin: { x: 62, y: 70, label: "11 events / 10y", tone: "amber" }, delay: 1100 },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
{ id: "streetview", name: "Streetview surveyor",
|
| 544 |
finding: "Multimodal: feeding Google Street View imagery to Gemma 4 vision to spot ground-floor flood-risk indicators.",
|
| 545 |
pin: null, delay: 1800 },
|
|
@@ -1117,12 +1123,29 @@ const mapDossier = (raw) => {
|
|
| 1117 |
streetview: raw.streetview || {},
|
| 1118 |
satellite: raw.satellite || {},
|
| 1119 |
local: raw.local || {},
|
|
|
|
| 1120 |
};
|
| 1121 |
};
|
| 1122 |
|
| 1123 |
const DossierScreen = ({ onBack, dossier }) => {
|
| 1124 |
const D = useMemo(() => mapDossier(dossier), [dossier]);
|
| 1125 |
const [showReasoning, setShowReasoning] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
const [showBoxes, setShowBoxes] = useState(true);
|
| 1127 |
const [imageTab, setImageTab] = useState("streetview"); // "streetview" | "satellite" | "topo"
|
| 1128 |
return (
|
|
@@ -1228,7 +1251,7 @@ const DossierScreen = ({ onBack, dossier }) => {
|
|
| 1228 |
const showOverlay = showBoxes && indicatorsWithBoxes.length > 0;
|
| 1229 |
return (
|
| 1230 |
<Section
|
| 1231 |
-
num="§03"
|
| 1232 |
icon={<Icon name="pin" color="var(--accent)" size={16}/>}
|
| 1233 |
iconBg="var(--accent-soft)"
|
| 1234 |
title="What we saw at the property"
|
|
@@ -1406,7 +1429,7 @@ const DossierScreen = ({ onBack, dossier }) => {
|
|
| 1406 |
})()}
|
| 1407 |
|
| 1408 |
<Section
|
| 1409 |
-
num={
|
| 1410 |
icon={<Icon name="warn" color="var(--coral)" size={16}/>}
|
| 1411 |
iconBg="var(--coral-soft)"
|
| 1412 |
title="Why FEMA's flood map isn't the whole story"
|
|
@@ -1544,8 +1567,90 @@ const DossierScreen = ({ onBack, dossier }) => {
|
|
| 1544 |
)}
|
| 1545 |
</Section>
|
| 1546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
<Section
|
| 1548 |
-
num={
|
| 1549 |
icon={<Icon name="drop" color="var(--accent)" size={16}/>}
|
| 1550 |
iconBg="var(--accent-soft)"
|
| 1551 |
title="The raw signals we looked at"
|
|
@@ -1559,7 +1664,7 @@ const DossierScreen = ({ onBack, dossier }) => {
|
|
| 1559 |
</Section>
|
| 1560 |
|
| 1561 |
<Section
|
| 1562 |
-
num={
|
| 1563 |
icon={<Icon name="news" color="var(--amber)" size={16}/>}
|
| 1564 |
iconBg="var(--amber-soft)"
|
| 1565 |
title="Recent local flood news">
|
|
@@ -1594,7 +1699,7 @@ const Chrome = ({ screen, onJump, dark, onToggleDark, language, onLanguageChange
|
|
| 1594 |
<div className="wordmark" onClick={()=>onJump("search")} style={{cursor:"pointer"}}>
|
| 1595 |
<span className="glyph">F</span>
|
| 1596 |
<span>FlutIQ</span>
|
| 1597 |
-
<span style={{color:"var(--ink-4)",fontSize:12,marginLeft:8,fontFamily:"JetBrains Mono"}}>v0.
|
| 1598 |
</div>
|
| 1599 |
<div className="chrome-meta">
|
| 1600 |
<span className="pill static"><span className="dot"/>gemma-4 · OpenRouter</span>
|
|
|
|
| 540 |
{ id: "archive", name: "Archivist",
|
| 541 |
finding: "Cook County: 11 NOAA flood events in 10yr, 4 FEMA disaster declarations. 2008, 2013, 2020, 2023 caused widespread basement flooding.",
|
| 542 |
pin: { x: 62, y: 70, label: "11 events / 10y", tone: "amber" }, delay: 1100 },
|
| 543 |
+
{ id: "regional", name: "Regional risk analyst",
|
| 544 |
+
finding: "FEMA NRI county-level multi-hazard profile — wildfire, hurricane, tornado, earthquake, etc. — for the wider neighborhood.",
|
| 545 |
+
pin: null, delay: 1300 },
|
| 546 |
+
{ id: "satellite", name: "Satellite surveyor",
|
| 547 |
+
finding: "Multimodal: bird's-eye Mapbox imagery → Gemma 4 vision → impervious-surface and catchment analysis.",
|
| 548 |
+
pin: null, delay: 1700 },
|
| 549 |
{ id: "streetview", name: "Streetview surveyor",
|
| 550 |
finding: "Multimodal: feeding Google Street View imagery to Gemma 4 vision to spot ground-floor flood-risk indicators.",
|
| 551 |
pin: null, delay: 1800 },
|
|
|
|
| 1123 |
streetview: raw.streetview || {},
|
| 1124 |
satellite: raw.satellite || {},
|
| 1125 |
local: raw.local || {},
|
| 1126 |
+
regional: raw.regional || {},
|
| 1127 |
};
|
| 1128 |
};
|
| 1129 |
|
| 1130 |
const DossierScreen = ({ onBack, dossier }) => {
|
| 1131 |
const D = useMemo(() => mapDossier(dossier), [dossier]);
|
| 1132 |
const [showReasoning, setShowReasoning] = useState(false);
|
| 1133 |
+
|
| 1134 |
+
// Section numbers depend on which conditional sections render. We
|
| 1135 |
+
// precompute them so the rendering JSX stays readable.
|
| 1136 |
+
const _sectionNums = useMemo(() => {
|
| 1137 |
+
const hasImages = D.streetview?.available || D.satellite?.available;
|
| 1138 |
+
const hasRegional = D.regional?.available;
|
| 1139 |
+
let n = 2; // §01 actions, §02 insurance fixed
|
| 1140 |
+
const fmt = () => "§" + String(++n).padStart(2, "0");
|
| 1141 |
+
return {
|
| 1142 |
+
images: hasImages ? fmt() : null,
|
| 1143 |
+
gap: fmt(),
|
| 1144 |
+
regional: hasRegional ? fmt() : null,
|
| 1145 |
+
signals: fmt(),
|
| 1146 |
+
news: fmt(),
|
| 1147 |
+
};
|
| 1148 |
+
}, [D]);
|
| 1149 |
const [showBoxes, setShowBoxes] = useState(true);
|
| 1150 |
const [imageTab, setImageTab] = useState("streetview"); // "streetview" | "satellite" | "topo"
|
| 1151 |
return (
|
|
|
|
| 1251 |
const showOverlay = showBoxes && indicatorsWithBoxes.length > 0;
|
| 1252 |
return (
|
| 1253 |
<Section
|
| 1254 |
+
num={_sectionNums.images || "§03"}
|
| 1255 |
icon={<Icon name="pin" color="var(--accent)" size={16}/>}
|
| 1256 |
iconBg="var(--accent-soft)"
|
| 1257 |
title="What we saw at the property"
|
|
|
|
| 1429 |
})()}
|
| 1430 |
|
| 1431 |
<Section
|
| 1432 |
+
num={_sectionNums.gap}
|
| 1433 |
icon={<Icon name="warn" color="var(--coral)" size={16}/>}
|
| 1434 |
iconBg="var(--coral-soft)"
|
| 1435 |
title="Why FEMA's flood map isn't the whole story"
|
|
|
|
| 1567 |
)}
|
| 1568 |
</Section>
|
| 1569 |
|
| 1570 |
+
{D.regional?.available && (
|
| 1571 |
+
<Section
|
| 1572 |
+
num={_sectionNums.regional}
|
| 1573 |
+
icon={<Icon name="warn" color="var(--purple)" size={16}/>}
|
| 1574 |
+
iconBg="var(--purple-soft)"
|
| 1575 |
+
title="Wider neighborhood — beyond just flooding"
|
| 1576 |
+
badge={<span className="risk-tag purple">FEMA NRI · county-level</span>}>
|
| 1577 |
+
<p style={{color:"var(--ink-2)", marginTop:0}}>
|
| 1578 |
+
FlutIQ is flood-focused, but your county faces other hazards too. This is FEMA's
|
| 1579 |
+
National Risk Index for {D.regional.nri?.county} {D.regional.nri?.state}
|
| 1580 |
+
{D.regional.nri?.population ? ` (pop. ${D.regional.nri.population.toLocaleString()})` : ""},
|
| 1581 |
+
calibrated against every other US county.
|
| 1582 |
+
</p>
|
| 1583 |
+
<div style={{display:"flex", gap:8, flexWrap:"wrap", margin:"12px 0", fontSize:12}}>
|
| 1584 |
+
{D.regional.nri?.composite_risk_rating && (
|
| 1585 |
+
<span className={`risk-tag ${
|
| 1586 |
+
D.regional.nri.composite_risk_rating.includes("Very High") ? "coral"
|
| 1587 |
+
: D.regional.nri.composite_risk_rating.includes("High") ? "amber"
|
| 1588 |
+
: D.regional.nri.composite_risk_rating.includes("Moderate") ? "amber"
|
| 1589 |
+
: "teal"
|
| 1590 |
+
}`}>
|
| 1591 |
+
Composite: {D.regional.nri.composite_risk_rating} ({D.regional.nri.composite_risk_score}/100)
|
| 1592 |
+
</span>
|
| 1593 |
+
)}
|
| 1594 |
+
{D.regional.nri?.expected_annual_loss_usd != null && (
|
| 1595 |
+
<span className="risk-tag neutral">
|
| 1596 |
+
Expected annual loss: ${(D.regional.nri.expected_annual_loss_usd / 1_000_000).toFixed(0)}M
|
| 1597 |
+
</span>
|
| 1598 |
+
)}
|
| 1599 |
+
{D.regional.nri?.social_vulnerability_rating && (
|
| 1600 |
+
<span className="risk-tag neutral">SoVI: {D.regional.nri.social_vulnerability_rating}</span>
|
| 1601 |
+
)}
|
| 1602 |
+
{D.regional.nri?.community_resilience_rating && (
|
| 1603 |
+
<span className="risk-tag neutral">Resilience: {D.regional.nri.community_resilience_rating}</span>
|
| 1604 |
+
)}
|
| 1605 |
+
</div>
|
| 1606 |
+
{Array.isArray(D.regional.headline_hazards) && D.regional.headline_hazards.length > 0 && (
|
| 1607 |
+
<div style={{marginBottom:12}}>
|
| 1608 |
+
<div className="stat-label" style={{marginBottom:8}}>Headline hazards for this county</div>
|
| 1609 |
+
<ol style={{margin:0, paddingLeft:0, listStyle:"none", fontSize:13, color:"var(--ink-2)", lineHeight:1.55}}>
|
| 1610 |
+
{D.regional.headline_hazards.map((h, i) => {
|
| 1611 |
+
const r = (h.rating || "").toLowerCase();
|
| 1612 |
+
const color = r.includes("very high") ? "var(--coral)"
|
| 1613 |
+
: r.includes("high") ? "var(--amber)"
|
| 1614 |
+
: r.includes("moderate") ? "var(--amber)"
|
| 1615 |
+
: "var(--teal)";
|
| 1616 |
+
return (
|
| 1617 |
+
<li key={i} style={{marginBottom:10, display:"flex", gap:10}}>
|
| 1618 |
+
<span style={{
|
| 1619 |
+
flexShrink:0, width:22, height:22, borderRadius:4,
|
| 1620 |
+
background:color, color:"white", fontSize:12, fontWeight:600,
|
| 1621 |
+
fontFamily:"'JetBrains Mono', monospace",
|
| 1622 |
+
display:"flex", alignItems:"center", justifyContent:"center",
|
| 1623 |
+
}}>{i+1}</span>
|
| 1624 |
+
<div>
|
| 1625 |
+
<strong>{h.name}</strong>
|
| 1626 |
+
{h.rating && <span className="small" style={{marginLeft:6}}> · {h.rating}</span>}
|
| 1627 |
+
{h.score != null && <span className="small" style={{marginLeft:6, color:"var(--ink-4)"}}> · score {h.score}</span>}
|
| 1628 |
+
<div style={{color:"var(--ink-3)", fontSize:12, marginTop:2}}>{h.matters_because}</div>
|
| 1629 |
+
</div>
|
| 1630 |
+
</li>
|
| 1631 |
+
);
|
| 1632 |
+
})}
|
| 1633 |
+
</ol>
|
| 1634 |
+
</div>
|
| 1635 |
+
)}
|
| 1636 |
+
{D.regional.what_property_level_flood_misses && (
|
| 1637 |
+
<p style={{margin:"10px 0", padding:"10px 12px", background:"var(--bg)", borderLeft:"3px solid var(--accent)", borderRadius:4, fontSize:13, color:"var(--ink-2)", lineHeight:1.55}}>
|
| 1638 |
+
<strong style={{color:"var(--accent)"}}>vs property-level FEMA · </strong>{D.regional.what_property_level_flood_misses}
|
| 1639 |
+
</p>
|
| 1640 |
+
)}
|
| 1641 |
+
{D.regional.resilience_context && (
|
| 1642 |
+
<p style={{margin:"10px 0", padding:"10px 12px", background:"var(--bg)", borderLeft:"3px solid var(--teal)", borderRadius:4, fontSize:13, color:"var(--ink-2)", lineHeight:1.55}}>
|
| 1643 |
+
<strong style={{color:"var(--teal)"}}>Community resilience · </strong>{D.regional.resilience_context}
|
| 1644 |
+
</p>
|
| 1645 |
+
)}
|
| 1646 |
+
<div style={{fontSize:11, color:"var(--ink-4)", marginTop:10, fontFamily:"'JetBrains Mono', monospace"}}>
|
| 1647 |
+
source · FEMA National Risk Index via the Resilience Analysis and Planning Tool (RAPT)
|
| 1648 |
+
</div>
|
| 1649 |
+
</Section>
|
| 1650 |
+
)}
|
| 1651 |
+
|
| 1652 |
<Section
|
| 1653 |
+
num={_sectionNums.signals}
|
| 1654 |
icon={<Icon name="drop" color="var(--accent)" size={16}/>}
|
| 1655 |
iconBg="var(--accent-soft)"
|
| 1656 |
title="The raw signals we looked at"
|
|
|
|
| 1664 |
</Section>
|
| 1665 |
|
| 1666 |
<Section
|
| 1667 |
+
num={_sectionNums.news}
|
| 1668 |
icon={<Icon name="news" color="var(--amber)" size={16}/>}
|
| 1669 |
iconBg="var(--amber-soft)"
|
| 1670 |
title="Recent local flood news">
|
|
|
|
| 1699 |
<div className="wordmark" onClick={()=>onJump("search")} style={{cursor:"pointer"}}>
|
| 1700 |
<span className="glyph">F</span>
|
| 1701 |
<span>FlutIQ</span>
|
| 1702 |
+
<span style={{color:"var(--ink-4)",fontSize:12,marginLeft:8,fontFamily:"JetBrains Mono"}}>v0.14 · beta</span>
|
| 1703 |
</div>
|
| 1704 |
<div className="chrome-meta">
|
| 1705 |
<span className="pill static"><span className="dot"/>gemma-4 · OpenRouter</span>
|