Stand at a stop in Dhaka and you have two questions. Which of these buses goes where I am going, and what should it cost me. Both answers are published. Neither is anywhere you can reach while standing at the stop.
The route charts are a PDF on a government site. The fare table is a scanned rate card inside that PDF. The railway timetable is on a ticketing portal that wants you online. The launch schedule is a gazette. The metro has its own site, and the thing you actually want — how much is left on my card — is only on the card.
Smart Route BD is the attempt to put all of it on a phone that is not online. This is what that took, what it does not do, and where every number in it came from.
Why build it at all
Because the failure mode is expensive in a way that does not show up in any dataset. If you do not know the fare before you get on, you pay what you are told. If you do not know which bus serves your route, you ask, and the answer depends on who you ask. Neither of those is solved by a map, and neither is solved by an app that needs a connection at the exact moment you are underground or out of credit.
So the design constraint was decided before anything else: every route, fare and timetable ships inside the app. Nothing is fetched to answer a question. The only network request the app makes at all is for a vehicle photo, and that is cached the first time you see it.
What is actually in it
Every figure here is counted from the JSON that ships in the bundle, not from the store listing:
| Mode | What ships | File |
|---|---|---|
| Bus | 183 operators with their stop lists in order | bus_data.json, 709 KB |
| Bus (official) | 112 gazetted routes across 387 named places, with per-stop distances | bus_routes_with_distance.json, 283 KB |
| Metro | MRT Line-6, 17 stations, chainage, headway bands, fare rules | metro_data.json, 31 KB |
| Rail | 224 trains, 417 stations, 45 lines, every halt with dwell time | railway-*.json, 894 KB |
| Launch | 88 vessels on 19 routes, from 35 operators | launch_data.json, 171 KB |
xychart-beta
title "What ships in the bundle"
x-axis ["Buses", "Bus stops", "Metro stations", "Trains", "Rail stations", "Launches"]
y-axis "Count" 0 --> 450
bar [183, 387, 17, 224, 417, 88]
Where every number came from
This is the part that took the time. Five datasets, five different problems, and each one lives in its own public repository so the work is checkable and reusable.
flowchart TD
P["BRTA fare-list PDF"] --> P1["Rendered to<br/>high-resolution images"]
P1 --> P2["OCR + LLM-assisted<br/>extraction and translation"]
P2 --> P3["dhaka-metro-area-bus-fare-list<br/>112 routes, per-stop distances"]
C["Community route lists<br/>+ Poribohon-BD image set"] --> C1["dhaka-city-local-bus-json-data<br/>156 operators, bilingual"]
R["BR timetable &<br/>e-ticketing portal"] --> R1["bd-railway-data<br/>224 trains, 417 stations"]
B["BIWTA notices, ghat boards,<br/>operator pages, news archives"] --> B1["bd-launch-json-data<br/>88 vessels, cross-checked"]
D["DMTCL timetable<br/>and fare rules"] --> D1["dhaka-metro-live-map<br/>chainage, headways, fares"]
P3 --> A["assets/ in the app"]
C1 --> A
R1 --> A
B1 --> A
D1 --> A
Bus fares — a PDF, rendered and read
BRTA publishes the Dhaka metropolitan fare list as a PDF of scanned rate cards. There is no API, no CSV, and the tables are images. The pipeline is unglamorous and it is the only one that works: render each page at high resolution, run OCR, then use an LLM pass to repair the table structure and carry the Bangla names across into English alongside the originals. The raw extraction is kept beside the cleaned output so a wrong cell can be traced back to the page it came from.
What that gives you is the thing the app needs and nobody else had: a distance for every stop on every gazetted route. With that, a fare is arithmetic rather than a lookup — km × ৳2.50, minimum ৳10, which is the published rate.
Bus routes — community data, and a research image set
Route membership is different from route distance. Which operators run a corridor, in what order they stop, and what service class they are, came from community-maintained lists, cleaned into dhaka-city-local-bus-json-data with English and Bangla names side by side. Vehicle photographs, so you can recognise the bus rather than read a side panel through traffic, come partly from Poribohon-BD, a published Bangladeshi vehicle image dataset, and partly from public sources.
Rail — a timetable with no tariff
Bangladesh Railway publishes its intercity timetable, and the extraction gives an unusually complete feed: 224 trains, 417 stations, 45 lines, every stoppage with arrival, departure, halt minutes and cumulative distance from origin.
What it does not give is fares. The feed's own metadata says so in a field: "faresIncluded": false.
Launch — the hardest one
River launches have no timetable API, and since the Padma Bridge opened most routes no longer run a daily per-vessel schedule at all — they run hull rotations, where which boat sails tonight depends on where it ended up yesterday. The dataset was assembled from BIWTA material, ghat timetables, operator pages, booking sites and news archives, and every entry was cross-checked against independent sources.
Where corroboration failed, the entry is not deleted and it is not quietly presented as fact. It is marked:
pie showData title The 88 launches, by how well corroborated
"active" : 67
"unverified" : 15
"suspended" : 5
"discontinued" : 1
Metro — the one with real numbers
DMTCL publishes a timetable, station chainage from Uttara North, and a fare rule. That rule is simple enough to implement exactly rather than approximate: minimum ৳20, ৳5 per kilometre, rounded up to the nearest ৳10, with 10% off for MRT Pass and Rapid Pass holders and 25% for the senior concession. The metro is the only mode in the app where the fares are not estimates.
The limitations, stated plainly
An app like this is only worth anything if it is honest about where its numbers are soft. These are the real ones.
| Limitation | Why | What the app does about it |
|---|---|---|
| Train fares are modelled | The timetable feed carries no tariff | Fares are fitted per class against published fares over a 15–552 km range, and every one is displayed with a ~ prefix |
| Some bus fares are heuristic | Not every operator's stop list matches a gazetted chart | Falls back to ~1.8 km per hop at the official rate, rounded up to ৳5 — and says it is an estimate |
| Launch schedules drift | Hull rotations, not fixed daily sailings | Status field per vessel; 15 marked unverified |
| No live vehicle positions | There is no public AVL feed for any mode | Nothing claims to be live except the metro, which says it is a simulation |
| Names are messy | One stop has three spellings across three sources | Stations and stops carry alias lists; matching normalises before comparing |
| Everything goes stale | Fares and timetables change without notice | A correction form in the app, and a disclaimer that says confirm at the counter |
That last row is worth being precise about, because it is easy to overclaim. The correction screen composes a pre-filled email and hands it to whatever mail app you have. It does not submit anything to a server, and the app cannot know whether you sent it — so the UI only ever says it is opening a draft. There is no moderation queue and no crowdsourced write path, because a transit dataset that anybody can edit is a transit dataset that is wrong in a new way every week.
~. The code comment that enforces it is blunter than the UI: in practice every fare the app shows is estimated.The metro map that is honest about being a simulation
There is no feed of where MRT-6 trains actually are. So rather than fake one or omit the feature, the app runs a simulation and labels it as one.
It is driven entirely by published facts: the official headway bands for the detected day type, the station chainage, and a Bangladesh Standard Time clock. Trains depart on the timetable's own headways, accelerate and decelerate between stations, and hold 30–60 seconds at each platform — longer through the morning and evening peaks. Concurrent trains are capped at 14, which is DMTCL's actual active fleet out of 24 six-coach sets.
The day type matters more than it sounds: the schedule differs across Sunday–Thursday, Friday, Saturday and government holidays, and getting that wrong makes the map confidently wrong rather than obviously wrong.
The same engine powers a standalone web version, which also draws the real surveyed alignment on OpenStreetMap tiles using geometry pulled from the Overpass API.
Reading the card, and never being able to spend from it
The feature people actually open the app for is the one that has nothing to do with routes: tap your MRT Pass or Rapid Pass to the back of the phone and see the balance.
These are FeliCa cards — JIS X 6319-4. The balance and the last twenty or so journeys sit in plain 16-byte blocks under one service, which means a single Read Without Encryption command gets them. No keys, no authentication, no server.
sequenceDiagram
participant U as You
participant P as Phone (NFC)
participant C as MRT / Rapid Pass
U->>P: Hold the card to the phone
P->>C: Polling, then Read Without Encryption
Note over P,C: Service 0x220F, 4 blocks at a time
C-->>P: 16-byte blocks: balance, journeys, top-ups
P->>P: Decode, map station codes to names
Note over P: Stored on the phone. Uploaded nowhere.
P-->>U: Balance, last journeys, last top-ups
The station codes in those blocks are the card's own numbering, and they include the Hatirjheel feeder-bus stops, because those share the Rapid Pass purse. Mapping them to names is a lookup table built by reading real cards.
Privacy, and the declaration that matches it
The app asks for three permissions: INTERNET, NFC and POST_NOTIFICATIONS. There is no location permission — unusual for a transit app, and possible only because nothing in it needs to know where you are.
| What | Where it goes |
|---|---|
| Routes, fares, timetables | Bundled. Browsing them contacts nothing |
| Favourites, recent searches, settings | On the phone, in Hive and SharedPreferences |
| Card readings | On the phone. Never uploaded |
| Vehicle photos | Fetched once from our image host, then cached. Carries an IP address and a file name, nothing else |
| Account | There isn't one |
The interesting part is the Play data-safety declaration, which says yes, data is collected — device identifiers, approximate location and ad interactions, shared for advertising. Not because we want it, but because the bundled ads SDK does, and the merged manifest proves it by injecting the AD_ID permission. The approximate location is declared even though the app requests no location permission at all, because an ad network derives coarse location from the IP address.
That declaration was written by reading the merged manifest rather than by describing intentions. An app that collects nothing itself but ships an ads SDK and declares "no data collected" is the exact mismatch Play enforces against, and it is an easy one to walk into honestly.
Unofficial, and saying so first
The app opens its own store listing with a disclaimer, before any feature: it is unofficial and independent, not affiliated with or endorsed by any ministry, authority or operator, and it sells no tickets and processes no payments. Authority names appear only to describe routes and to credit sources — with the source links in the app, pointing at BRTA, DMTCL, Bangladesh Railway, BIWTA and DTCA.
This is not lawyer decoration. An app that shows government fare tables, reads a transit card and uses authority names is one ambiguous screenshot away from looking like an official service, and the people most likely to be misled are the ones with the least room to absorb the mistake.
What happened to the data afterwards
The five datasets are public repositories, not app internals, and that turned out to matter. Another developer has since built smartroutebd.com — a web version covering the same four modes, built on the same published data, crediting the same repositories.
That is the correct outcome. The app was never the valuable part; roughly 2 MB of cleaned JSON is. Anybody who wants to build a better reader for it can, and the fare list, the route charts and the launch dataset outlive whatever happens to any particular app.
What I would tell anyone doing this for another city
- The data exists. It is just not in a form you can query. Assume PDFs, scans, gazettes and notice boards, and budget for extraction rather than integration.
- Separate the dataset from the app, publicly, from day one. It makes the numbers checkable, it makes them reusable, and it is the part that keeps its value.
- Mark what you could not verify. A record flagged
unverifiedis more useful than the same record deleted and far more honest than the same record presented as fact. - Label every estimate, in the UI, every time. A tilde in front of a number costs nothing and is the difference between a tool and a liability.
- Ship it offline. The moment you need a lookup is disproportionately the moment you have no connection.
- Do not fake live data. Simulate it from the timetable and say that is what you did, or leave it out.
- Read your own merged manifest before filling in the data-safety form. What your dependencies collect is what you collect.
Links
- Smart Route BD on Google Play — free, offline, Android.
- dhaka-city-local-bus-json-data — Dhaka bus operators, routes and bilingual names.
- dhaka-metro-area-bus-fare-list — the BRTA fare list, extracted, with a fare calculator.
- bd-railway-data — Bangladesh Railway timetables, stations and stoppages.
- bd-launch-json-data — 88 river launches, with a verification trail.
- dhaka-metro-live-map — the MRT-6 simulation, as a web app.
- The product page — full feature list, screenshots and FAQ.
Always confirm at the counter before you travel. That sentence is in the app, on the listing and in this article for the same reason: everything above is a best effort at a moving target, assembled from sources that were themselves a best effort.