Skip to content
GuidesBlogPlaygroundDashboard

Find Food Deserts in a City

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.

  • 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

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);
};

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.

The complete app is at 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.

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.