Explorinator combines hiking routes from multiple major aggregators into a single searchable map.
I used to be a big fan of gpsies.com. Gpsies hosted routes from individual contributors, which meant you could see clusters of routes in a given area and plan trips around them. After Gpsies was acquired by Alltrails, I lost that ability. Alltrails is a better service in many ways, but it does not let you see all available routes at once on a map and neither does any other major aggregators I know of (Komoot, Wikiloc, Outdooractive, etc.).
Inspired by waymarkedtrails.org, I built a web application that renders routes directly on a map. Unlike the major aggregators, routes do not need to be loaded individually. Instead, map tiling is used to load both the base map and the routes for a given area simultaneously, making it possible to see all available routes at once. While Waymarked Trails uses static bitmap tiles, this project uses vector tiles, which enables frontend styling and interactivity.
One of the key features of route aggregators is search. With static tiles this is not feasible, but since the introduction of Martin it is possible to pass query parameters and dynamically fetch vector tiles. This project exposes filters for route length, route type (loop vs. point-to-point), and data source. Users can adjust these filters and watch the map update in real time.
To populate the application, I first wrote an ingestion script for OpenStreetMap, assuming local governments would have uploaded their trails there. Coverage turned out to be uneven, some provinces (in the Netherlands) are well-represented while others contain little useful data (note to Dutch local governments: please upload your public trails to OSM). Because most high-quality route data is locked behind commercial aggregators, I also wrote ingestion scripts for several other sources and was able to collect all their "publicly" accessible data. As this is likely a legal grey area, I am not sharing that code or the data. It is, however, very much possible.
Routes are styled using MapLibre and rendered in the colors defined by their original source. Because styling happens client-side, colors remain consistent across the app and the map stays interactive. Where routes overlap, clicking any point on the map brings up a list of all routes passing through it. Selecting a route from the list opens a detail view with metadata and a direct GPX download. A feature that is frustratingly locked behind a paywall on most major aggregators.
The application has been running well. The application has 140k+ routes from various sources collected across the Netherlands, with filtering fast enough to run on a resource-limited NAS. After using it for a while, though, I think I understand why the major aggregators don't offer an "all routes" view: there can be too many routes in certain areas even after filtering, showing them all clutters the map and can make it hard to find anything actually worth hiking.
This raises an interesting question about curation. Aggregators likely rank by popularity, which creates a feedback loop where less-discovered routes never surface. I see two ways around this: better filtering, or shifting the model entirely. Using routes not as the primary object but as a signal for where good paths exist, and dynamically generating routes from that graph. The latter is almost certainly what Komoot does behind the scenes, judging by their data, and it is the more elegant solution.
In the future I may play around with clustering and filtering based on terrain type, but for now I am happy to have a working prototype that scratches an itch I have had for years.
graph TD
Browser["Browser"]
Caddy["Caddy\n:80 · :443"]
Proxy["Nginx\n:80"]
Frontend["Angular\n:80"]
API["FastAPI\n:8000"]
Martin["Martin tile server\n:3000"]
DB[("PostgreSQL + PostGIS")]
Ingest["Ingest scripts\nOSM and others"]
Browser -->|HTTPS| Caddy
Caddy --> Proxy
Proxy -->|"/"| Frontend
Proxy -->|"/api/"| API
Proxy -->|"/martin/"| Martin
API --> DB
Martin -->|"vector tiles"| DB
Ingest -.->|"offline"| DB
![]() All routes around Deventer |
![]() Filters for length, route type and data source |
![]() Clicking the map shows matching routes on that point |
![]() Detail view of a single route |
![]() Every hiking route in the Netherlands |
|
Start the full stack with:
docker compose upThis starts Nginx, the Angular frontend, the FastAPI service, the Martin tile server, and PostgreSQL. See the individual READMEs for running each part in isolation or for ingesting data:




