Skip to content
GuidesBlogPlaygroundDashboard

Build a Multi-Stop Road Trip Planner

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.

  • 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

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,
}

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.

The complete app (FastAPI backend + Leaflet map) is at 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.

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