WanderLust
A-to-B routes through places you'll love
Track: Backyard AI · Live Space: https://huggingface.co/spaces/build-small-hackathon/WanderLust
This is an app built by myself and my teammate Tristan as a submission for the Huggingface Hackathon.
Every navigation app we've used solves the same problem: get me there in the minimum time. WanderLust starts from the opposite premise. We're cyclists, and crossing an unfamiliar city we kept hitting the same frustration which is that the map only ever knows the fastest line between two points, but the whole joy of exploring is the bookshop, the quiet square, the viewpoint you'd never have found on the direct route. If you'd happily spend fifteen extra minutes, that surplus is a budget — and the interesting question is what to spend it on!
So WanderLust takes a start, a destination, a free-text vibe, and an adventurousness level, and returns a walkable or bikeable route that deliberately detours past places matching your taste within a hard travel-time budget plus a narrated itinerary explaining why each place is on your path. The route never exceeds (1 + budget) × the direct time; budget 0 simply gives you the plain route. It runs on a 1B model and OpenStreetMap data, across nine cities, with no cloud API calls at request time.
Routing a path isnt actually the hard problem here, it's the gap between how people describe what they want and what a router can optimize. Someone types "a slow Sunday-morning kind of walk" or "bookshops and quiet streets," and something has to turn that unbounded human mood into concrete, scored weights across seventeen place categories plus quiet/green/lively modifiers. No lookup table or keyword list maps open-ended language onto a route. The model is the bridge and then it writes the itinerary that explains, in your own words, why each chosen stop matches what you asked for.
Small is the point, not a constraint to grumble about. Routing stays pure classical algorithm; the model is load-bearing in exactly two places —> interpretation and narration — and both fit a 1B. Your taste profile never has to leave the Space; there are no accounts and no external inference API.
The strategy we committed to on the build log's first line: walking skeleton first, scariest plumbing early, AI added only after a manual-weight router already works. Each brick had a definition-of-done and a test, and wasn't left until green. The first bricks contained no AI at all.
The boring-but-scary plumbing came first: download the Paris walk network via OSMnx into a graph of 77,454 nodes and 221,688 edges (a 90 MB GraphML), geocode inputs, run Dijkstra, render the polyline on a map. Then the POI layer: 30,357 Paris POIs across 17 curated categories, each with greenness/quietness priors and a confidence score derived from OSM tag richness.
The heart of the system is classical, and it's where the engineering went. We model the detour as the Orienteering Problem — prize-collecting with fixed endpoints, NP-hard in general and solve it with a budgeted greedy heuristic over a submodular reward. The key modeling insight is diversity by design. A naive scorer that just sums point values will happily route you past five cafés, because cafés are everywhere and each scores fine on its own. The submodular reward applies diminishing returns per category: the first café is worth full value, the second much less, the third almost nothing. So a park + a viewpoint + a bookshop beats five cafés not from a hand-tuned penalty, but because the objective itself says variety is worth more than repetition. The solver runs two greedy passes: by raw marginal gain and by reward-per-added-time and keeps the higher-reward feasible result; a single ratio pass alone gets trapped hoarding cheap duplicates. It's tested against a known-optimal synthetic instance, with an explicit diversity-beats-repetition test.
Only once that manual-weight router produced real discovery routes on a real map did any model enter the picture.
Two models, both deliberately small, each load-bearing in exactly one place — with graceful fallbacks so the app never breaks:
openbmb/MiniCPM5-1B (1B parameters, standard LlamaForCausalLM, no custom kernels) does two jobs from one set of weights. Call 1 turns your free-text vibe into a scored JSON of category weights. Call 2 writes the first-person itinerary narration. It runs inside the Space on ZeroGPU via @spaces.GPU, weights pulled straight from the Hub — no external inference API, nothing leaves the Space.BAAI/bge-small-en-v1.5 (~33M parameters, CPU-only) is the fallback interpreter: when no GPU is allocated, the vibe is embedded and matched by cosine similarity against a gloss for each of the 17 categories.The interpreter is a tiered dispatcher — MiniCPM5-1B → bge-small embeddings → a model-free keyword net → neutral — and every tier returns the same {category: affinity} shape, so the routing engine is oblivious to which one ran. The model is what makes the experience feel like it read your mind; the fallbacks are what keep it standing when ZeroGPU is busy. Narration has the same property: a deterministic, grounded-by-construction template is always available, and the LLM only rewrites it.
Letting an LLM narrate a route is exactly the place a hallucination hurts most: the model will cheerfully invent a charming bistro that doesn't exist, and a user might walk there. So narration sits behind a fail-closed grounding verifier. It extracts capitalized place-name spans from the generated text — handling multi-word names and French "de la" chains, and treating "and"/"et" as list-joiners that separate names rather than glue them — and the text passes only if every mention maps to an allowed name: the route's actual waypoints, the start/end, or a curated per-city gazetteer of districts, rivers, and landmarks. Any violation, and the system silently falls back to the deterministic template. The release-gate test plants a hallucinated "Eiffel Tower" and verifies the gate catches it.
The interesting part was getting the gate right, and it took a real adversarial pass to find the holes:
Paris ships full-city. The other eight — London, Barcelona, Berlin, New York, San Francisco, Tokyo, Mumbai, Shanghai — are baked offline as bounded walkable cores and hosted as an open Hub dataset (build-small-hackathon/discoverroute-cities), pulled and pre-warmed into memory at boot so the first user to pick a city waits zero seconds. With DISCOVERROUTE_OFFLINE=1 (the deployed config) there are no cloud API calls at request time: geocoding resolves against the local POI index, routing runs on the cached graph, and the model runs in-Space. The whole interpretation-and-narration stack — and the template path entirely fits on a laptop.
A few things broke, and what fixing them taught us:
build_matrix recomputed three times inside the alternatives loop. Hoisting it out (compute once, reuse) dropped three alternative routes from ~2.1 s to ~1.3 s — the cost of a single route. Lesson: measure on a quiet machine before believing a profile, and look for repeated work before clever work."180s requested vs. 170s left." ZeroGPU reserves the requested duration seconds per call, so asking for a fat 120 s slice drains a day's allowance in a dozen requests. A 1B model loading from cache and generating ≤480 tokens on an A10G finishes well inside 45 s, so we ask for that — roughly 3× more calls per day. Reading the actual production traces, not guessing, is what found it.These last two only surfaced because we logged every inference call to a trace dataset (build-small-hackathon/discoverroute-traces) — which turned debugging the live Space from guesswork into reading rows.
The default Gradio look got replaced wholesale. WanderLust runs on a hand-built HTML/CSS/JS app-shell served by Gradio's gr.Server FastAPI backend and called from the browser via @gradio/client — no default Gradio components: a custom map window with its own titlebar, custom controls, a live-map loader. The fiddliest part: the map is an iframe, and an iframe can't be animated from the parent page. So the route draw-on and the staggered marker pops are injected as JavaScript inside the iframe's own document, keyed off CSS classes attached to the polylines and markers at render time. Reduced-motion preferences and focus rings are respected throughout.
Two product details we care about, because a discovery route is only worth planning if you can actually keep it.
Your taste, remembered. You can save a standing taste profile — free-text preferences plus the categories of places you've saved — and WanderLust blends that with each trip's mood instead of asking you to re-describe yourself every time. A handful of saved places nudges the route toward what you like, with a saturating boost so a few saves matter but a hundred don't swamp the trip's actual vibe. True to the "small, local-first" spirit, there are no accounts: the profile lives on your device, never on a server.
Your route, taken with you. A plan you can't follow is just a pretty picture, so every result carries a Take it with you row built entirely client-side from the plan payload — no extra round-trip:
The GPX is the lossless option, and the UI says so honestly: when a route has more stops than a Google Maps link can hold, it tells you the GPX keeps all of them. You plan the interesting way here, then walk or ride it with whatever navigation you already trust.
A discovery router where the AI is genuinely load-bearing but small enough to run in one Space; a classical orienteering core that earns diversity from its objective rather than a hack; a fail-closed grounding gate hardened by real adversarial review; nine cities offline; a custom frontend; and two open datasets — city cores and inference traces — left on the Hub for anyone to reuse.
The spend-your-extra-time-on-discovery idea turned out to need surprisingly little model and a surprising amount of careful plumbing. That felt like the right ratio.
A-to-B routes through places you'll love