Build a Neighborhood Cafe Finder
A React app that geocodes an address, finds nearby cafes with outdoor seating, calculates walk times, and plots everything on an interactive map.
Type an address, get a map showing every cafe with outdoor seating within walking distance, sorted by how long it takes to get there on foot.
What you’ll use
Section titled “What you’ll use”- Geocoding to turn an address into coordinates
- PlazaQL to find cafes with outdoor seating nearby
- Distance matrix to calculate walking time from you to each cafe
- MapLibre GL JS to plot everything
Key code
Section titled “Key code”Geocoding + PlazaQL + distance matrix
Section titled “Geocoding + PlazaQL + distance matrix”The core logic chains three API calls: geocode the address, query for nearby cafes, then get walking times to all of them in one batch.
import Plaza from "@plazafyi/sdk";
const client = new Plaza();
export async function geocode(address) { const result = await client.v1.geocode.forward({ q: address, limit: 1 }); const [lng, lat] = result.features[0].geometry.coordinates; return { lat, lng };}
export async function findCafes(lat, lng) { const query = `$$ = search(node, amenity: "cafe", outdoor_seating: "yes").around(distance: 800, geometry: point(${lat}, ${lng}));`; const result = await client.v1.query({ query }); return (result.features || []).map((f) => ({ id: f.properties?.osm_id, name: f.properties?.name || "Unnamed Cafe", lat: f.geometry.coordinates[1], lng: f.geometry.coordinates[0], cuisine: f.properties?.cuisine || null, }));}
export async function getWalkTimes(originLat, originLng, cafes) { const result = await client.v1.matrix({ origins: [{ lat: originLat, lng: originLng }], destinations: cafes.map((c) => ({ lat: c.lat, lng: c.lng })), mode: "foot", }); return cafes.map((cafe, i) => ({ ...cafe, walkMinutes: Math.round(result.durations[0][i] / 60), distanceMeters: result.distances[0][i], }));}Search flow in the React component
Section titled “Search flow in the React component”The App component chains the three calls and passes results to the map and list:
const handleSearch = async (e) => { e.preventDefault(); setLoading(true);
const location = await geocode(address); setCenter(location);
const nearbyCafes = await findCafes(location.lat, location.lng); const cafesWithTimes = await getWalkTimes(location.lat, location.lng, nearbyCafes); setCafes(cafesWithTimes);
setLoading(false);};Plotting markers on MapLibre
Section titled “Plotting markers on MapLibre”Each cafe gets a marker. Clicking one flies the map to it:
cafes.forEach((cafe) => { const el = document.createElement("div"); el.textContent = "\u2615"; el.addEventListener("click", () => onSelect(cafe.id));
new maplibregl.Marker({ element: el }) .setLngLat([cafe.lng, cafe.lat]) .addTo(map);});How it works
Section titled “How it works”Geocode turns the address into [lng, lat] coordinates via /v1/geocode.
PlazaQL finds every node tagged amenity=cafe + outdoor_seating=yes within 800 meters. The .around() filter handles the radius search, and the response comes back as GeoJSON.
Distance matrix calculates walk times to all cafes in a single request — one origin (you), N destinations (the cafes). Much faster than routing to each one individually.
Sort and display — combine cafe data with walk times, sort by shortest walk, render a clickable list alongside the map.
Full source
Section titled “Full source”The complete app is at github.com/plazafyi/example-apps/tree/main/cafe-finder.
Built with React + Vite + MapLibre GL JS.
Variations to try
Section titled “Variations to try”Filter by cuisine. Add cuisine: "coffee" to the PlazaQL query to find proper coffee shops, or cuisine: "italian" for Italian cafes.
Expand the radius. Change distance: 800 to distance: 1500 in the .around() call for a wider search. You’ll get more results but longer walk times.
Switch to cycling. Change the matrix profile from foot to bicycle and rename “walk time” to “bike time”. Same code, different mode.
Filter by capacity. Use an expression filter to only show cafes with seating capacity above a threshold: search(node, amenity: "cafe", outdoor_seating: "yes").around(distance: 800, geometry: point(${lat}, ${lng})).filter(number(t["capacity"]) > 20). The number() cast is required because tag values are strings.
Look up by ID. If you have a list of known cafe OSM IDs, fetch them directly: search(node, id: [12345, 67890, 11111]).
Add opening hours. Many cafes in OSM have an opening_hours tag. Check cafe.properties.opening_hours to show whether each place is currently open. Parsing OSM opening hours is its own adventure — the opening_hours.js library handles the format.