Build a Cycling Trail Finder App
Discover bike-permitted paths and off-road trails in any area using OpenStreetMap data. This example demonstrates OR-combined tag filtering with | inside tags - to surface trails where cycling is explicitly permitted, whether as informal paths through nature or unpaved forestry tracks.
What you'll build
A map layer showing all cycling-permitted trails in a given area - ideal for MTB apps, gravel cycling route planners, or any outdoor recreation feature. The result is a GeoJSON FeatureCollection of LineString features you can render directly as a styled overlay in Leaflet, Mapbox, or any GeoJSON-capable map library.
The key technique here is the | operator inside a tags value: you specify that the feature must have bicycle=yesorbicycle=designated - so both informally permitted and formally designated cycling ways are captured in one request.
Prerequisites
Run these commands on a Bash terminal.
pip install osmfeaturesScript
View full source code: test_cycling_trails.py
"""
App idea: adventure cycling layer showing unpaved trails where cycling is
explicitly permitted — ideal for MTB or gravel bike route planning.
OSM tags used:
- ``highway=path`` + ``bicycle=yes`` or ``bicycle=designated``
- ``highway=track`` + ``bicycle=yes`` or ``bicycle=designated``
API calls use one ``tags`` value with ``|`` for either bicycle permission.
"""
from osmfeatures import OSMFeature, OSMFeatureCollection, OSMFeaturesClient
DJURGARDEN_BBOX = "18.090,59.320,18.170,59.345"
def test_cycling_trails(client: OSMFeaturesClient):
# Paths with cycling explicitly permitted (bicycle=yes or bicycle=designated)
paths_cycling = client.query(
bbox=DJURGARDEN_BBOX,
type="way",
way_shape="line",
tags=["highway=path", "bicycle=yes|bicycle=designated"],
limit=50,
)
# Dedicated cycling tracks (unpaved forestry-style)
tracks = client.query(
bbox=DJURGARDEN_BBOX,
type="way",
way_shape="line",
tags=["highway=track", "bicycle=yes|bicycle=designated"],
limit=50,
)
assert isinstance(paths_cycling, OSMFeatureCollection)
assert isinstance(tracks, OSMFeatureCollection)
all_trails = paths_cycling["features"] + tracks["features"]
assert len(all_trails) > 0, "Expected cycling trail features in Djurgården"
for f in all_trails:
assert isinstance(f, OSMFeature)
assert f.osm_type == "way"
assert f["geometry"]["type"] == "LineString"
tags = f.tags
hw = tags.get("highway")
assert hw in ("path", "track", "cycleway"), f"Unexpected highway tag: {hw}"
# Every returned trail must carry an explicit cycling-permitted tag
assert tags.get("bicycle") in ("yes", "designated"), (
f"Trail {f['id']} (highway={hw}) lacks explicit bicycle access: "
f"bicycle={tags.get('bicycle')!r}"
)
print(f"\n[cycling trails] {len(all_trails)} trail segments in Djurgården")
How it works
The query combines repeated tags values (AND) with | inside one value (OR). Understanding that difference is the key to building expressive queries:
| Parameter | Value | Semantics |
|---|---|---|
tags | highway=path | AND filter - the feature must have this tag. Multiple tags values are all required. |
tags | bicycle=yes|bicycle=designated | OR filter - join alternatives with | inside one value. The feature must match at least one. A separate tags value is ANDed with the others. Requires a spatial anchor. |
type | line | Returns only LineString geometries - the correct type for routable ways. |
highway=path for informal unpaved paths and highway=track for wider forestry / agricultural tracks. These are distinct highway classes - a single tags=highway=path call won't return tracks. Making two requests and merging the results mirrors the Overpass approach of querying multiple way types in a union. requests library accepts a list of (key, value) tuples for the params argument. This is the correct way to send repeated query parameters like tags=highway=path&tags=bicycle=yes|bicycle=designated. Adapt it
Change the tags values to find other types of route infrastructure:
| Use case | tags (repeat for AND) |
|---|---|
| Dedicated cycleways | highway=cycleway |
| Hiking trails | highway=path and foot=yes|foot=designated |
| Horse riding trails | highway=bridleway |
| Ski pistes | piste:type=downhill |
More examples
Park Bench Finder
The simplest possible MapLark query - fetch point nodes in a bbox and place them on a map.
Restaurant Guide
Fetch restaurants with names, cuisine types, and map pins - handling both point and polygon geometries.
Walking Route Planner
Build Dijkstra-based walking directions on a real OSM footway network.
API Playground
Explore the OSM dataset with preset tag filters and test all API parameters.