--- title: Build a Multi-Stop Road Trip Planner | Plaza Docs description: A Python script that geocodes cities, optimizes visit order, calculates the full route, finds interesting stops along the way, and prints a detailed itinerary. --- Plan a road trip across multiple cities: geocode the stops, let Plaza figure out the best visit order, route each leg, and find viewpoints, parks, and historic sites along the way. The example app wraps this in a FastAPI backend with a Leaflet map frontend. ## What you’ll use - **Geocoding** to turn city names into coordinates - **Route optimization** to reorder stops for minimum travel time (TSP solver) - **Routing** to get the actual driving path for each leg - **PlazaQL** to find points of interest near each leg’s route ## Key code The interesting part is the pipeline: geocode, optimize, route each leg, then query for POIs along it. **Optimize the visit order.** Give Plaza a list of waypoints and it solves the Traveling Salesman Problem. `roundtrip=False` means you don’t need to return to the start — the optimizer picks the best ordering. ``` waypoints = [{"type": "Point", "coordinates": [g["lng"], g["lat"]]} for g in geocoded] order = await plaza_client.optimize_waypoints(client, waypoints) ordered = [geocoded[i] for i in order] ``` **Route each leg and collect the geometry.** The route response includes the full polyline, which we use to build a bounding box for the POI search. ``` rt = await plaza_client.route(client, origin, dest) dist_km = rt["distance_m"] / 1000 dur_hr = rt["duration_s"] / 3600 ``` **Find interesting stops along the route.** Buffer the route geometry by 10km and run a PlazaQL query for six POI categories. The `name` filter on peaks avoids returning every unnamed bump in the terrain. ``` bbox = plaza_client.route_bbox(rt["geometry"], buffer_km=10.0) query = f""" $viewpoints = search(node, tourism: "viewpoint").bbox({s}, {w}, {n}, {e}); $parks = search(node, boundary: "national_park").bbox({s}, {w}, {n}, {e}); $museums = search(node, tourism: "museum").bbox({s}, {w}, {n}, {e}); $historic = search(node, historic: *).bbox({s}, {w}, {n}, {e}); $waterfalls = search(node, waterway: "waterfall").bbox({s}, {w}, {n}, {e}); $peaks = search(node, natural: "peak", name: *).bbox({s}, {w}, {n}, {e}); $$ = $viewpoints + $parks + $museums + $historic + $waterfalls + $peaks; """ ``` If you need the POI categories as separate layers (for color-coding on a map, for example), use named outputs instead. Later expressions can reference earlier named outputs with `$$.name`: ``` query = f""" $$.viewpoints = search(node, tourism: "viewpoint").bbox({s}, {w}, {n}, {e}); $$.parks = search(node, boundary: "national_park").bbox({s}, {w}, {n}, {e}); $$.museums = search(node, tourism: "museum").bbox({s}, {w}, {n}, {e}); $$.historic = search(node, historic: *).bbox({s}, {w}, {n}, {e}); $$.waterfalls = search(node, waterway: "waterfall").bbox({s}, {w}, {n}, {e}); $$.peaks = search(node, natural: "peak", name: *).bbox({s}, {w}, {n}, {e}); """ ``` **Build the bounding box from route coordinates.** A simple min/max over the polyline coordinates with a buffer converts km to approximate degrees. ``` def route_bbox(geometry, buffer_km=10.0): coords = geometry.get("coordinates", []) lngs = [c[0] for c in coords] lats = [c[1] for c in coords] buf = buffer_km * 0.009 # ~0.009 degrees per km return { "south": min(lats) - buf, "west": min(lngs) - buf, "north": max(lats) + buf, "east": max(lngs) + buf, } ``` ## How it works The optimize endpoint takes your waypoints and returns them reordered to minimize total driving time. The response includes a `waypoint_index` on each feature that maps back to your original input array, so you can match names to the new ordering. For each consecutive pair in the optimized list, we get a route and then search for POIs within a bounding box around that route. The bounding box approach is fast but rough — some POIs will be miles from the actual road. A tighter approach would sample points along the polyline and use `.around()` queries, but the bbox works well enough for trip planning. ## Full source The complete app (FastAPI backend + Leaflet map) is at **[github.com/plazafyi/example-apps/tree/main/road-trip-planner](https://github.com/plazafyi/example-apps/tree/main/road-trip-planner)**. Stack: Python, FastAPI, Leaflet, Plaza API. The backend handles all the Plaza calls and returns structured JSON; the frontend renders the route and POIs on the map. ## Variations to try **Round trip.** Pass `roundtrip=True` to the optimize call to plan a loop that returns to your starting city. **Gas stops.** For legs over 400km, find fuel stations near the midpoint: ``` query = f""" $$ = search(node, amenity: "fuel").around(distance: 2000, geometry: point({mid_lat}, {mid_lng})); """ ``` **Campground finder.** Swap the POI categories for `tourism: "camp_site"`, `tourism: "caravan_site"`, `leisure: "nature_reserve"`. **Daily itinerary.** Split the trip into 8-hour driving days. After each 8 hours of cumulative driving, find the nearest city and mark it as an overnight stop. **Export to Google Maps.** Generate a URL with waypoints in the optimized order: ``` base = "https://www.google.com/maps/dir/" points = "/".join(f"{wp['coordinates'][1]},{wp['coordinates'][0]}" for wp in ordered) print(f"Open in Google Maps: {base}{points}") ```