Hiking routes are relations tagged route=hiking that group the paths of a trail. Joined into one line and sent to the elevation profile endpoint, they give the distance, the ascent and descent, and a profile to draw.
The Matterhorn Trail runs from Schwarzsee at the foot of the Matterhorn down to Furi above Zermatt.
The query
[out:json][timeout:25]; rel["route"="hiking"]["name"="Matterhorn-Trail"](45.95,7.65,46.05,7.80); out geom;
Run it
Live result from overpass.ch, cached for up to 12 hours. Map data © OpenStreetMap contributors (ODbL). Heights produced using Copernicus WorldDEM™-90 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved. Sea depths: GEBCO Compilation Group (2026) GEBCO 2026 Grid.
How it works
- The bounding box makes the name unambiguous: many trails share a name.
out geom;returns the relation with the geometry of each path.- Join the members with an empty,
forwardorbackwardrole, in order. Leave outalternativeandexcursionmembers, which are side trips. - The profile endpoint samples the line every 90 m and adds up the climb and the descent. In this data the trail is about 7.3 km long and descends about 750 m, from Schwarzsee at about 2,580 m to Furi at about 1,860 m.
Variations
- Joining works only if the members are listed in order along the trail. Some relations are not: check that the joined line is about as long as all ways together, or sort the ways by their shared end points first.
- Ascent and descent leave out changes of less than 2 m to filter noise. A 90 m grid also smooths short ups and downs, so on a winding trail the real climb is somewhat higher.
- Find the routes of an area first: hiking routes in a region lists them with their names. Then select one by name, or by its ID:
rel(19448726); out geom;.
Run it from your code
Two requests: the query returns the route, and its joined geometry goes to /elevation/profile.
import math
import requests
API = "https://api.overpass.ch/v1/YOUR_API_KEY"
query = open("query.overpassql").read()
r = requests.post(f"{API}/interpreter", data={"data": query}, timeout=120)
r.raise_for_status()
route = r.json()["elements"][0]
def gap(a, b): # distance in degrees, enough to compare
return math.hypot(a[0] - b[0], (a[1] - b[1]) * math.cos(math.radians(a[0])))
ways = [[[p["lat"], p["lon"]] for p in m["geometry"]] for m in route["members"]
if m["type"] == "way" and m["role"] in ("", "forward", "backward")]
path = list(ways[0])
for i, way in enumerate(ways[1:]):
near = lambda p: min(gap(p, way[0]), gap(p, way[-1]))
if i == 0 and near(path[0]) < near(path[-1]):
path.reverse() # the first way points backwards
if gap(path[-1], way[-1]) < gap(path[-1], way[0]):
way = way[::-1] # so does this one
path += way
r = requests.post(f"{API}/elevation/profile", json={"path": path}, timeout=60)
r.raise_for_status()
answer = r.json()
print(answer["profile"]) # distance_m, ascent_m, descent_m, min_m, max_m, …
for p in answer["results"][:5]:
print(p["distance"], p["elevation"])
const API = "https://api.overpass.ch/v1/YOUR_API_KEY";
const res = await fetch(`${API}/interpreter`, { method: "POST", body: new URLSearchParams({ data: query }) });
if (!res.ok) throw new Error(await res.text());
const route = (await res.json()).elements[0];
// distance in degrees, enough to compare
const gap = (a, b) => Math.hypot(a[0] - b[0], (a[1] - b[1]) * Math.cos((a[0] * Math.PI) / 180));
const ways = route.members
.filter((m) => m.type === "way" && ["", "forward", "backward"].includes(m.role))
.map((m) => m.geometry.map((p) => [p.lat, p.lon]));
const path = [...ways[0]];
ways.slice(1).forEach((way, i) => {
const near = (p) => Math.min(gap(p, way[0]), gap(p, way.at(-1)));
if (i === 0 && near(path[0]) < near(path.at(-1))) path.reverse(); // the first way points backwards
if (gap(path.at(-1), way.at(-1)) < gap(path.at(-1), way[0])) way.reverse(); // so does this one
path.push(...way);
});
const pr = await fetch(`${API}/elevation/profile`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
});
if (!pr.ok) throw new Error(await pr.text());
const { profile, results } = await pr.json();
console.log(profile, results.slice(0, 5));
Save the query as query.overpassql. It works the same in Overpass Turbo, QGIS and JOSM.