time budget
GET /explore.json?lat&lng&radius=500&minutes=40[&speed=20]
→ length_min = 0.5 × budget, length_max = budget, plus a painless filter
distance + elevation_gain × 10 ≤ budget (budget = speed_kmh × minutes / 60 × 1000)
→ meta.explore.time_box = { minutes, speed_kmh, speed_source: param|history|default, climb_factor: 10, distance_budget }
speed_source history = median avg_speed of your last 20 gps rides
inside the lines
GET /explore.json?area=lat,lng|lat,lng|lat,lng[|…]&minutes=40 (3 to 20 corners)
→ the polygon is filled with H3 res-10 hexes (cap 17,000 ≈ 15 km × 15 km, sampled to 1,024 terms)
→ elasticsearch containment: min_match_containment 0.9 (area_containment 0.5..1.0 to tune)
→ meta.explore.area = { vertices, hex_count, min_containment, centroid, bounding_box }
q cannot combine with area (both use the es score)
build a loop
GET /explore/stitch.json?lat&lng&minutes=40[&speed][&radius=500]
→ 30 candidates ≤ 3 × budget · junction ≤ 60 m · leg ≥ 1 km · overlap ≤ 35 % · target 0.9 × budget · best 5
→ { stitches: [ { legs: [ { route_id, from_index, to_index, reversed, distance, elevation_gain } ], distance,
elevation_gain, effective_distance, estimated_minutes, junction, overlap_pct, polyline } ], candidates: [...] }
the polyline is plain google lat/lng at 1e-5 (two values per point)
POST /explore/stitch/save.json { legs, name?, minutes, start, junction? }
→ 201 { route: { id, url, name, distance, elevation_gain, visibility, pinned: true }, sources }
rebuilt from the two source tracks, private, owned by you, pinned. 401 signed out, 400 bad legs.
the polyline
route simplified_polyline: SIX delta ints per point: y ×1e-5, x ×1e-5, d (m ×1e-2), e (m ×1e-2), S ×1e-1, R ×1e-1
this page decodes it in the browser and uses d for length and e for climb, exactly as rwgps-ui does.