Inspiration

Google's Routes API accepts exactly one travel mode per request. It will chain walking and transit, but it will never tell you to take a CNG to the metro station. So a Dhaka commuter never sees the option that is often both cheaper and faster than the one they're shown.

That gap isn't a routing problem — it's a local cost-data problem. Google can't scale "what does a rickshaw from Dhanmondi 27 to Kalabagan actually cost at 6pm" to 200 countries. We only need one city.

What it does

Poth enumerates mixed-mode journeys across Dhaka — bus to the station, metro for the long leg, rickshaw at the other end — prices every leg in taka, and surfaces the money/time/comfort trade-off instead of hiding it.

Dhanmondi 27 → Uttara Sector 7

Route Cost Time
Bike (Pathao) ৳230 51 min ← what every other app shows
Bus → Metro → Bus ৳87 1h 42m ← saves ৳143
Bus ৳47 1h 49m

Three things make it work:

1. Ask in Banglish. Type Farmgate theke Uttara, taka bachate chai but 40 min er beshi na and Gemini sets your value-of-time weight, your duration cap, and your mode exclusions. Bus e jabo na removes the bus.

2. Comfort is a real axis. Public bus is always cheapest, so a planner that only weighs money and time recommends it every single time — which is not how anyone actually travels. Every mode carries a comfort score, and discomfort is weighted by time spent in it: 90 minutes standing on a packed bus is penalised far harder than 10.

3. A Pareto front, not a single answer. We keep every itinerary that nothing else beats on cost, time and comfort simultaneously, then label Fastest / Best value / Cheapest / Most comfortable.

How we built it

Next.js 16 + React 19 + MUI 9 with hand-built Material 3 tonal palettes, on FastAPI + PostgreSQL/PostGIS + Redis. Everything in Docker; one command brings up the whole stack.

The planner is deterministic arithmetic, not an LLM:

  1. Ellipse prune on transfer nodes with origin and destination as foci — 2k routing calls, not k²
  2. Enumerate direct baselines + every viable access-mode × transit × egress-mode combination (~270 itineraries per query)
  3. Score in taka and minutes, add time-weighted discomfort
  4. Three-objective Pareto filter
  5. Rank by generalized_cost = fare + vot × minutes + comfort × discomfort_minutes

Gemini sits at the edges only — parsing Banglish queries, extracting trip logs from one line of free text, and normalising informal addresses (emk tower → EMK Center, Mohammadpur) before a real geocoder resolves them. It never plans a route, never estimates a fare, and never outputs coordinates, because it would invent plausible-but-wrong ones confidently.

Fully non-Google by design: OSM data, Photon/Nominatim geocoding, MapLibre. The Maps Platform terms forbid both deriving content from Maps data and the caching that makes our fan-out affordable.

Challenges we ran into

The bus problem. Our first version recommended the bus for literally every query, because it is always cheapest and money alone cannot express why people avoid it. Adding comfort as a third Pareto axis — rather than a tiebreak — was the fix; with only two axes the bus owns the cheap corner and every more pleasant option looks dominated.

Ownership constraints. An owned motorbike can only appear before a transit leg, because it's parked at the station. Cars can't park at MRT-6 stations at all, so they're kiss-and-ride only. Get this wrong and you emit nonsense like "own car to station, metro, own car to destination."

Dhaka place names end in numbers. Our fare parser was stripping trailing digits as noise and turning Dhanmondi 27 into Dhanmondi — but Gulshan 1 and Gulshan 2 are different places. Now it only strips a trailing number when it matches the fare it already detected.

A CSS layer beat our map. Unlayered maplibre-gl.css overrode MUI's layered styles and forced the map container to zero height — an invisible collapse that looked like a tile-loading failure.

Accomplishments that we're proud of

The value-of-time slider is the product. Drag it and the recommendation genuinely flips between a ride-hail and a mixed bus/metro/rickshaw chain. Same for comfort — at weight 0 you get the bus, at weight 4 you get Bike → Metro → Bike.

Everything degrades instead of breaking. No Gemini key → keyword heuristics. No database → bundled seed data. No geocoder → 30 curated landmarks. No router → haversine estimate. The demo cannot go dark.

Every fare is labelled by confidence. Metro and bus use published DMTCL and BRTA rates; CNG, rickshaw and ride-hail are clearly marked as estimates. We'd rather show an honest range than a confident wrong number.

What we learned

The moat is fare data, not routing. Routing is a commodity. What nobody has is what a rickshaw actually costs at 6pm — which is why trip logging is one line of Banglish, parsed server-side, rather than a six-field form nobody would ever fill in.

And LLMs belong at the edges. Gemini is genuinely great at code-mixed Bengali-Latin text that regex will never handle. It is the wrong tool for the middle, where you need the same answer every time in microseconds.

What's next for Poth

Self-hosted Valhalla to replace the public OSRM demo. A fare-correction pipeline that pulls crowdsourced trip logs into per-route adjustments with confidence scoring. Bus route data. MRT Lines 1 and 5 when they open — at which point enumeration stops scaling and we move to MCR (Multimodal Multicriteria RAPTOR).

Built With

Share this project:

Updates

Submission history