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 - the same expressive query surface as Overpass - 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 or_tags parameter: 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 ``or_tags`` to match either bicycle permission value.
"""
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",
shape="line",
tags="highway=path",
or_tags=["bicycle=yes", "bicycle=designated"],
limit=50,
disable_budget_warning=True,
)
# Dedicated cycling tracks (unpaved forestry-style)
tracks = client.query(
bbox=DJURGARDEN_BBOX,
type="way",
shape="line",
tags="highway=track",
or_tags=["bicycle=yes", "bicycle=designated"],
limit=50,
disable_budget_warning=True,
)
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 uses three different filter parameters with distinct semantics. Understanding the difference between tags and or_tags 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. |
or_tags | bicycle=yes | OR filter - the feature must match at least one or_tags value. Repeat the parameter for each alternative. Requires a spatial anchor. |
or_tags | bicycle=designated | |
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 or_tags=bicycle=yes&or_tags=bicycle=designated. Adapt it
Change the tags and or_tags combination to find other types of route infrastructure:
| Use case | tags | or_tags |
|---|---|---|
| Dedicated cycleways | highway=cycleway | (none needed) |
| Hiking trails | highway=path | foot=yes, foot=designated |
| Horse riding trails | highway=bridleway | (none needed) |
| Ski pistes | piste:type=downhill | (none needed) |
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.