Skip to content
GuidesBlogPlaygroundDashboard

Map Emergency Service Coverage

A Python script that finds all fire stations in a city, generates 5-minute driving isochrones from each, and reports what percentage of the city is covered.

Can a fire truck reach your house in 5 minutes? This script pulls every fire station from Plaza, generates a 5-minute driving isochrone from each one, calculates what percentage of the city is covered, and renders an interactive Folium map showing the gaps.

  • Geocoding to find the city center
  • PlazaQL to locate all fire stations (nodes, ways, and relations)
  • Isochrone API to generate 5-minute driving polygons from each station
  • Grid sampling with ray casting to estimate coverage percentage

A PlazaQL query finds every fire station in a 12km radius. Stations can be mapped as nodes, ways, or relations, so all types are queried:

import plaza
client = plaza.Client()
query = (
'$$ = search(amenity: "fire_station")'
f'.around(distance: 12000, geometry: point({lat}, {lng}));'
)
fc = client.v1.query(data=query)

For each station, a driving isochrone shows everywhere reachable within the NFPA 1710 standard of 5 minutes. This accounts for speed limits, turn restrictions, and one-way streets:

for station in stations:
iso_fc = client.v1.isochrone(center={"type": "Point", "coordinates": [station["lng"], station["lat"]]}, contours_minutes="5", mode="auto")
polygons.extend(extract_polygons(iso_fc))

Coverage is calculated by sampling a grid of points across the city bounds and checking which fall inside any isochrone polygon using ray casting:

def calculate_coverage(bounds, polygons, step=0.003):
covered, uncovered = [], []
for lat, lng in generate_grid(bounds, step):
if point_in_any_isochrone(lat, lng, polygons):
covered.append((lat, lng))
else:
uncovered.append((lat, lng))
pct = len(covered) / (len(covered) + len(uncovered)) * 100
return pct, covered, uncovered

The uncovered points become red cells on the Folium map, making gaps immediately visible:

m = folium.Map(location=[center_lat, center_lng], tiles="cartodbpositron")
for iso_fc in isochrone_geojsons:
folium.GeoJson(iso_fc, style_function=lambda f: {
"fillColor": "#22c55e", "fillOpacity": 0.25
}).add_to(m)
for lat, lng in uncovered_points:
folium.Rectangle(
bounds=[[lat - half, lng - half], [lat + half, lng + half]],
fill_color="#ef4444", fill_opacity=0.3,
).add_to(m)

The 5-minute standard comes from NFPA 1710, which specifies a 90th-percentile response time for urban fire departments. This tool measures geographic coverage (can a truck get there in 5 minutes), not actual response time (which adds dispatch and turnout delays). It’s a useful proxy for identifying underserved areas.

Grid sampling approximates continuous coverage with discrete points at ~300m intervals. The ray casting point-in-polygon test checks each point against every isochrone. This is simple but effective — for a typical city you’ll sample 2,000-5,000 points, which runs in seconds.

The complete Python project with modular coverage calculation, Folium map builder, and CLI is on GitHub:

plazafyi/example-apps/emergency-coverage

Stack: Python, Folium (interactive maps), Plaza SDK. Run with python main.py "Portland, Oregon". Pass a second argument for custom response times: python main.py "Austin, TX" 8.

Other emergency services. Swap amenity=fire_station for amenity=police or amenity=hospital. Hospitals have different response time standards (the “golden hour” for trauma), but the same isochrone technique works.

Multi-contour analysis. Generate 3-minute, 5-minute, and 8-minute isochrones from each station in a single request to see tiered coverage rings.

Optimal new station placement. Find the largest contiguous cluster of uncovered cells. Its centroid is a reasonable candidate for a new station — a simplified facility location analysis.

Population-weighted coverage. Raw geographic coverage treats empty fields the same as dense apartment blocks. Weight each grid cell by census population to surface the gaps that matter most.