project 01 / 03
StopFinder
How far is the walk from this flat to the nearest tram stop? A Chrome extension for flat hunting in Kraków that answers it right on the listing, using open timetable and map data instead of paid map APIs.
- Java 21
- Spring Boot
- PostGIS
- OSRM
- Docker
- JavaScript
The idea
When you're flat hunting, listings rarely tell you how well connected a place really is. I wanted that answer right next to the listing: the nearest bus and tram stop, and how long it takes to walk there.
A straight line on a map isn't enough. The closest stop can be on the other side of railway tracks, a river or a fenced estate. So StopFinder measures the real walking route, and it does it without paid map APIs: the stops come from the city's open timetable data and the routes from OpenStreetMap.
What it does
- Open a flat listing on Otodom or OLX, click the extension, and it finds the flat's location on the page by itself. On other pages you can use your current location instead.
- Shows the nearest bus stop and the nearest tram stop, with the walking distance and the time it takes at an easy pace.
- Picks the stop that is closest on foot, which isn't always the one that looks closest on the map. Across random points in Kraków, that happens almost one time in four.
- Says when a listing only gives an approximate location, and falls back to straight-line distance, clearly marked, if the route server is down.
- One script downloads Kraków's latest timetables and map, prepares everything and loads the stops, so the data is easy to refresh.
Under the hood
Kraków publishes its timetables as three open GTFS feeds: city buses, trams and the Mobilis bus lines. The backend imports all three in one transaction, so a broken file can't leave the database half-loaded. Each stop gets its type by following stop times → trips → routes, and a stop served by both buses and trams is stored once for each, so either search finds it.
Finding the stop is a two-step search. PostGIS uses its KNN operator <-> on a GiST index to pick the 8 nearest stops of each type in a straight line. Then a single request to OSRM, a routing engine running on the OpenStreetMap map of Kraków with a walking profile, measures the real route to all of them at once, and the shortest walk wins.
-- candidates of one type: fast index lookup, then exact metres
SELECT k.name, k.lat, k.lon,
ST_Distance(k.geom::geography, :point::geography) AS distance_m
FROM (
SELECT s.name, s.lat, s.lon, s.geom
FROM stops s
WHERE s.type = :type
ORDER BY s.geom <-> :point -- KNN on the GiST index
LIMIT :limit + 10
) k
ORDER BY distance_m
LIMIT :limit;
The index sorts by distance in degrees, and in Kraków a degree of longitude is only about 0.64 of a degree of latitude. That's why the query takes a few extra candidates from the index and re-sorts them by the true distance in metres before anything goes to OSRM.
The database and OSRM run in Docker. The schema lives in init.sql, with the point column generated by Postgres itself, and Hibernate only validates that the entities match it. The backend has unit tests, tests of the OSRM client against a stub HTTP server, and integration tests on a real PostGIS database in Testcontainers. The extension's location parsers are tested in Node.
What's next
- Finding the location from the listing's address when the page has no coordinates.
- Caching results, so opening the same listing again is instant.