65 million NFIRS fire & EMS incident records (1980–2025), processed to Parquet on Cloudflare R2 and queried in-browser via DuckDB-WASM — with zero backend cost for per-department queries.
NFIRS is voluntary — roughly 70% of U.S. departments participate. We treat these caveats as first-class metadata, not footnotes. They travel with every query and surface persistently in the UI rather than being buried in a disclaimer.
NFIRS is voluntary. Raw counts are never presented as authoritative state or national totals. Every aggregate carries an “estimate” badge and a coverage note. The UI enforces this — there is no opt-out.
What NFIRS collected changed twice. EMS data is absent 1980–1998 and again 2004–2013.
Call-composition charts are gated behind source_release_scope metadata stamped at ETL time — the UI cannot render a
donut for those windows.
2012: format shifted from dBASE to delimited text. 2018/19: the NFIRS incident key was
dropped — UIDs are now synthesized by hashing STATE + FDID + INC_DATE + INC_NO + EXP_NO. Trend lines crossing these
years show the break explicitly.
NFIRS was retired in January 2026 — the corpus is frozen. All computation happens at ETL time or in the browser. There is no running database server in production. Pre-computing aggregates once is cheaper and faster than serving a live query engine.
NFIRS changed its collection scope twice over 45 years. The frontend is built to never smooth over these breaks — scope bands travel as metadata from ETL to every chart. Hover the timeline to explore.
Shape A queries — single-department detail — run entirely in DuckDB-WASM in the user's browser. R2 SQL is a fallback for genuinely novel cross-department queries, not the primary path. The frozen corpus makes pre-computation cheap and reliable.
Novel or national queries that can't be served from pre-computed aggregates fall back to R2 SQL — Cloudflare's managed query engine at ~$2.50 per TB scanned. This is the exception. Pre-computation handles 95%+ of queries at zero cost.
Shape A queries never leave the browser. Shape B queries check pre-computed aggregates first, then fall back to R2 SQL only if needed. Click a query type to trace how it routes.
Row-group pruning, not partition explosion. We partition by year and state only, then sort
within partitions by fdid + incident_date. DuckDB can skip entire row groups
without downloading them — a typical per-department query fetches under 50 MB from a 100+ GB
state partition.
dept_year_incidenttype.parquet per-dept per-year rollupstate_year_incidenttype.parquet national / state-level rollupdept_incidenttype_alltime.parquet lifetime totals per deptThe sort order is the key insight. DuckDB reads only the row groups where dept_fdid matches the query, skipping the rest without downloading them. A separate
fdid partition would create file-count explosion; sorting within state partitions achieves
the same pruning at a fraction of the object count.
Every displayed canonical field is tagged by current source compatibility: NFIRS-backed,
expected to map to NERIS, derived by Emberline, or not yet confirmed. This follows the
canonical schema in 03-data-model.md and the adapter-seam strategy in 04-neris-transition.md.
| Canonical field | Status | Display meaning | Compatibility note |
|---|---|---|---|
source_system | Derived by Emberline | Origin system for each canonical row. | Stamped as NFIRS today and becomes the split key for future NERIS rows. |
incident_uid | Derived by Emberline | Stable canonical incident identifier. | Synthesized from source keys so NFIRS and NERIS can share one serving shape. |
dept_fdid / state / dept_name | NFIRS-backed | Reporting department identity and geography. | Backed by NFIRS FDHeader-era identifiers; later NERIS department crosswalk remains to be confirmed. |
incident_date / year | Expected NERIS map | Normalized incident date and year partition. | NFIRS-backed today; expected to map directly as the shared trend axis. |
alarm_ts / arrival_ts / cleared_ts | Expected NERIS map | Response-time timestamps for duration analysis. | NFIRS-backed where present; expected NERIS operational times need adapter validation. |
incident_type_code / label | NFIRS-backed | Raw source incident type and decoded source label. | NFIRS-specific code vocabulary; NERIS should not leak its raw equivalent into frontend queries. |
incident_category | Expected NERIS map | Source-agnostic kind: fire, EMS, hazmat, service, false alarm, etc. | Canonical vocabulary is intentionally anchored toward anticipated NERIS families. |
source_release_scope | Derived by Emberline | Per-row scope band used to gate all-services charts. | Derived from NFIRS release-year bands; future NERIS scope semantics are not yet confirmed. |
lat / lon / coordinate_* | Not yet confirmed | Map coordinates and precision/source provenance. | NFIRS geocoded source decision is still blocked; NERIS coordinate availability is unknown. |
outcome severity fields | Expected NERIS map | Fatalities, injuries, property loss, and acres burned. | NFIRS-backed today; expected to remain an orthogonal outcome overlay in NERIS. |
module flags / casualties | Not yet confirmed | Module presence and nested/analytics casualty records. | NFIRS module-backed today; NERIS child-record shape must be mapped after public release. |
NFIRS was retired January 2026. We designed the pipeline so that only one component changes when NERIS data lands — the Mapper. The storage format, serving layer, and frontend queries are unchanged.
The CanonicalIncident schema stays fixed. Both NfirsMapper and NerisMapper produce the same output shape. The Loader,
R2 storage layout, and DuckDB-WASM serving layer see no difference.
Emberline can visualize NFIRS and NERIS data side-by-side across the 2026 transition boundary — both normalized into the same canonical schema.