--- title: Find Food Deserts in a City | Plaza Docs description: A Python script that divides a city into a grid, measures distance to the nearest grocery store for each cell, and outputs a GeoJSON map of food desert areas. --- A food desert is an area where the nearest grocery store is more than 1 mile away (the USDA’s urban threshold). This app divides a city into a grid, queries Plaza for every supermarket and grocery store in the area, calculates distance from each cell to its nearest store, and visualizes the result as a 3D column map — taller and redder means worse access. ## What you’ll use - **Geocoding** to find the city center - **PlazaQL** to locate all grocery stores and supermarkets from OSM - **Isochrone** (optional) to show a 15-minute walking radius when you click a cell - Client-side haversine math for the nearest-store calculation ## Key code The analysis has three steps: find stores, build a grid, measure distances. **Query every grocery store in the area.** A single PlazaQL call pulls supermarkets, grocery stores, and greengrocers within a radius of the city center. ``` $supermarkets = search(node, shop: "supermarket").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $grocery = search(node, shop: "grocery").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $greengrocers = search(node, shop: "greengrocer").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $ways = search(way, shop: "supermarket").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $ways_grocery = search(way, shop: "grocery").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $$ = $supermarkets + $grocery + $greengrocers + $ways + $ways_grocery; ``` If you want to analyze store types separately (for example, to see whether an area has supermarkets but no greengrocers), use named outputs. Each `$$.name` assignment produces a separate feature collection in the response, and later expressions can reference them with `$$.name`: ``` $$.supermarkets = search(node, shop: "supermarket").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $$.grocery = search(node, shop: "grocery").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); $$.greengrocers = search(node, shop: "greengrocer").around(distance: ${radiusM}, geometry: point(${lat}, ${lng})); ``` **Generate a grid and find the nearest store for each cell.** The grid uses \~400m cells. For each one, brute-force haversine against every store to find the closest. It’s O(cells \* stores) but fast at city scale — typically a few hundred cells and under 100 stores. ``` for (let lat = minLat; lat <= maxLat; lat += GRID_STEP) { for (let lng = minLng; lng <= maxLng; lng += GRID_STEP) { // Skip cells outside the circular radius if (haversineM(centerLat, centerLng, lat, lng) > radiusM) continue; let minDist = Infinity; let nearest = null; for (const store of stores) { const d = haversineM(lat, lng, store.lat, store.lng); if (d < minDist) { minDist = d; nearest = store; } } cells.push({ lat, lng, distanceToStore: minDist, classification: classify(minDist) }); } } ``` **Classify and color each cell.** The color ramp goes from green (store within 400m) through yellow to red (over 1 mile). Column height uses an exponential curve so food deserts really pop out of the map. ``` function getElevation(distanceM: number): number { const normalized = Math.min(distanceM / 3500, 1); return normalized * normalized * 2000 + 20; // exponential curve } ``` **Click a cell for a walking isochrone.** When you click any grid column, the app fetches a 15-minute walking isochrone from that point, showing how far you could actually walk to get groceries. ``` const handleGridClick = (info) => { const cell = info.object; fetchIsochrone(cell.lat, cell.lng).then(setIsochrone); }; ``` ## How it works The whole analysis runs client-side. The app makes two Plaza API calls — one geocode and one PlazaQL query — then does the grid math in the browser. deck.gl renders each cell as a 3D column with height and color mapped to distance-to-nearest-store. The 1-mile threshold comes from USDA research on urban food access. The classification goes: excellent (< 400m), good (400-800m), moderate (800m-1mi), food desert (1-2mi), severe (> 2mi). One thing to keep in mind: OSM is volunteer-contributed, so coverage varies. A missing store in the data doesn’t mean there’s no store there. And physical distance doesn’t tell the whole story — a store across a highway with no pedestrian crossing is farther than haversine suggests. ## Full source The complete app is at **[github.com/plazafyi/example-apps/tree/main/food-desert-finder](https://github.com/plazafyi/example-apps/tree/main/food-desert-finder)**. Stack: Vite, React, TypeScript, deck.gl, MapLibre GL JS, Plaza API. Everything runs in the browser except the Plaza API calls. ## Variations to try **Tighter grid.** Change `GRID_STEP` to `0.002` for \~200m cells. More detail, more computation. **Include convenience stores.** Add `search(node, shop: "convenience")` to the PlazaQL query. They aren’t full grocery stores but they do provide basic food access. You could weight them at 0.5x. **Population weighting.** This analysis treats all cells equally. A food desert in a dense residential area matters more than one in an industrial zone. If you have census data, weight each cell by population. **Compare cities.** Run it on multiple cities and compare the food desert percentages. Portland vs. Houston vs. Atlanta. The patterns often correlate with urban planning decisions — though that analysis goes beyond what OSM data alone can tell you.