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.
What you’ll use
Section titled “What you’ll use”- 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
Key code
Section titled “Key code”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, uncoveredThe 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)How it works
Section titled “How it works”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.
Full source
Section titled “Full source”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.
Variations to try
Section titled “Variations to try”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.