SPMT
A public-transit platform for Târgoviște: journey planning and a live map for riders, plus an admin console so the transit authority can edit stops, routes, and timetables.

What it is
On the surface, a bus line is just a list of stops. But underneath, it is two directions, variants per direction for school days and weekends and holidays, and a shape on the map that has to follow real streets instead of drawing straight lines between pins. If any one of those things is wrong, a planner can make you wait for a bus that never comes.
The project I built has two sides. Riders get a trip planner that handles transfers, a map with every line and stop on it, departure boards, and a small corner for announcements and fares. The transit authority gets a console to edit the whole network, from stops and variants to the geometry on the map, the timetables, and the days when a given trip does not run.
How it works
The project is a Go backend, React and Postgres, plus a self-hosted OSRM instance so bus paths can follow real roads.
I wrote the routing myself around RAPTOR. Pick a date and it first works out which trips actually run that day and builds a timetable out of them. Then it searches in rounds, where each new round is allowed one more transfer than the last, and it keeps going until another transfer stops helping.
Each round looks at every route touching a stop that got better in the last round. It boards the earliest trip you can still catch and carries the better arrival times downstream. Transfers cost extra minutes, so the planner never asks you to sprint between buses in 40 seconds. It also refuses to put you back on the route pattern you just stepped off.
I also check a few nearby departure times around what the user asked for. Results get cached in memory by date and origin-destination pair.
The admin console has an AI assistant that can look things up and propose edits, but I didn’t want it writing to the database on its own. So reads run right away, while writes pile up in a pending plan instead. Each proposed change carries a snapshot of what was there before, and nothing gets applied until a person approves the whole plan.
Where it got hard
Divided streets were the most challenging, the kind with one lane each way and a concrete median down the middle. OSRM kept snapping stops onto the wrong side of the road and drawing little U-turn loops in the middle of routes.
My first instinct was to shrink the snap radius. That made it worse. Stop pins are almost never exactly on the asphalt, so a tight radius just left stops unmatched or yanked them somewhere stranger. What finally worked was telling OSRM which way the bus is moving. Every stop gets the compass heading from the stop before it, and the bearings parameter keeps the approach within 90 degrees of that heading.
bearings := make([]string, len(stops))
bearings[0] = ""
for i := 1; i < len(stops); i++ {
heading := bearingBetween(stops[i-1], stops[i]) // degrees clockwise from north
bearings[i] = fmt.Sprintf("%d,90", int(math.Round(heading)))
}
query.Set("bearings", strings.Join(bearings, ";"))
// Example: ";217,90;225,90;198,90"What I'd change
The timetable cache still lives in the server process. With a single instance that holds up fine, except the first search after a deploy has to rebuild the whole day before it can answer. Precomputing it at startup would reduce that delay.