Long Earth SocietyLong Earth Society

Misc · Prototype · Jul 2026

Halftank.

A map for e-bikes, mopeds and small scooters that answers one question: with the range I have, where can I ride and still get home, on roads I'm actually allowed or willing to ride? Car maps don't know. It turns out one speed limit changes the answer by a factor of four.

Two reach maps from downtown Mesa on a 20-mile round trip. With a 35 mph ceiling, a lopsided patch of residential streets is reachable and most of the grid is grey. With a 45 mph ceiling, the whole diamond-shaped area is colored.
35 mph ceiling · 1,590 road-miles45 mph ceiling · 6,029 road-miles
SPEED
LIMIT
≤20
SPEED
LIMIT
25
SPEED
LIMIT
30
SPEED
LIMIT
35
SPEED
LIMIT
40
SPEED
LIMIT
45
SPEED
LIMIT
50+
over the ceiling
Same start (downtown Mesa), same 20-mile round trip, real OpenStreetMap roads. On the left the vehicle won't go on anything posted above 35; on the right, 45. Grey is road you could reach by distance but not at that speed. The diamond shape is the street grid: ten miles of driving on a grid gets you about ten miles along an axis, not along a diagonal.
In reach at 35 mph
1,590 mi
In reach at 45 mph
6,029 mi
Road segments
686,219
Speed limit mapped
36.6%

Why

It started with a practical question: how far can a 50cc scooter that tops out at 40 mph actually get around the East Valley? On a map, everything looks close. On the ground, nearly every through-road in the Phoenix suburbs is posted at 45, and a small scooter, an e-bike or a moped either isn't allowed on them or shouldn't be. The distance is fine; the roads are the problem.

Existing tools answer the wrong question. Isochrone maps show how far you get in 20 minutes by car, freeways included. Golf-cart routing apps follow 35 mph roads but don't know your range. Nobody draws range and road speed together, which is exactly the decision a small-vehicle rider makes before leaving.

How it works

Halftank is a single static web page: no server, no API key. You drop a pin, pick a vehicle, and it does four things in the browser.

  1. Pull the roads. It asks OpenStreetMap's public query service for every road in a box around the pin, with geometry and tags.
  2. Read the limit. Each segment gets a speed: the posted maxspeed if someone mapped it, otherwise a default by road type, flagged as a guess.
  3. Build a graph. Every point where segments meet becomes a node; every segment becomes an edge weighted by its length in miles.
  4. Search outward. Dijkstra's shortest-path algorithm expands from the pin, skips every edge above the vehicle's ceiling, and stops when the distance budget runs out. Round trip halves the budget.
The core: Dijkstra with a speed ceilingjavascript
// Shortest-path outward from the start, but some edges simply don't exist for you.
function reach(graph, start, budgetMiles, ceilingMph) {
  const dist = new Map([[start, 0]]);
  const pq = new MinHeap([[0, start]]);
  while (pq.size) {
    const [d, u] = pq.pop();
    if (d > dist.get(u) || d > budgetMiles) continue;
    for (const { to, miles, mph } of graph.get(u)) {
      if (mph > ceilingMph) continue;            // too fast for this vehicle: not an option
      const nd = d + miles;
      if (nd <= budgetMiles && nd < (dist.get(to) ?? Infinity)) {
        dist.set(to, nd);
        pq.push([nd, to]);
      }
    }
  }
  return dist;                                    // every node you can reach, and how far it took
}

const budget = roundTrip ? range / 2 : range;    // there and back without stopping

The result isn't a circle with a radius. It's the real network: dead ends, canals, freeway crossings and all. Reachable roads are colored by their posted limit, and the legend is a row of speed-limit signs you can tap to hide a class. Roads you could reach by distance but not at your speed are drawn grey and dashed, so you can see what the ceiling costs you.

SPEED
LIMIT
≤20
SPEED
LIMIT
25
SPEED
LIMIT
30
SPEED
LIMIT
35
SPEED
LIMIT
40
SPEED
LIMIT
45
SPEED
LIMIT
50+
over the ceiling
The legend doubles as a filter: tap a sign to hide that class.
Vehicle presets: range in miles, top speed you'll ridejavascript
const PRESETS = {
  ebike_class2: { range: 35,  ceiling: 30 },
  ebike_class3: { range: 45,  ceiling: 35 },
  moped:        { range: 80,  ceiling: 35 },
  scooter_50cc: { range: 90,  ceiling: 45 },
  lsv:          { range: 30,  ceiling: 35 },
  motorcycle:   { range: 150, ceiling: 999 },
};

What one speed limit costs

35 MPH≤20 mph: 763 road-miles25 mph: 712 road-miles30 mph: 38 road-miles35 mph: 77 road-miles1,590 mi45 MPH≤20 mph: 3,079 road-miles25 mph: 1,972 road-miles30 mph: 120 road-miles35 mph: 272 road-miles40 mph: 234 road-miles45 mph: 353 road-miles6,029 miROAD-MILES IN REACH, BY POSTED LIMIT
Road-miles in reach, colored by posted limit. Raising the ceiling from 35 to 45 doesn't just add the 45 mph roads (353 miles of them); it connects everything behind them. Reach nearly quadruples.

Two honest caveats sit inside those numbers. First, a lot of the "≤20" miles are parking aisles, alleys and service drives, which count as road in the data but not in a rider's head. Second, only 37% of road segments in this area have a speed limit in OpenStreetMap at all. The other 63% are guessed from the road's type. The page shows that percentage live, because it's the honest data-quality verdict for whatever area you're looking at.

When the limit isn't mappedjavascript
// Most roads have no posted limit in OpenStreetMap. Fall back to the road's class.
const ASSUMED = {
  motorway: 65, trunk: 55, primary: 45, secondary: 40, tertiary: 35,
  unclassified: 30, residential: 25, living_street: 15, service: 15,
};
function mph(tags) {
  const m = /(\d+)/.exec(tags.maxspeed || "");
  if (m) return { mph: +m[1], mapped: true };
  return { mph: ASSUMED[tags.highway] ?? 25, mapped: false };   // counted toward "% guessed"
}

Where it stands

It's a working prototype. The open problems are the ones the numbers point to: better speed-limit data (city and county GIS layers publish posted limits; OpenStreetMap is incomplete), one-way streets (the graph is currently undirected), and junction matching by OpenStreetMap node instead of rounded coordinates. The query service also slows down past about 15 miles out, so a public version would need its own routing backend.

What I learned

  • Constraints beat distance. For a small vehicle, the question isn't how far but which roads. One rule (no roads over 35) removed three-quarters of the reachable network.
  • Show the cost. Drawing the forbidden roads in grey turned an abstract setting into something you can see.
  • Publish the data quality. A map built 63% on guesses should say so on the screen.
  • Grids make diamonds. On a street grid, "ten miles" is a diamond, not a circle.

Map data © OpenStreetMap contributors. Figures computed Oct 18, 2026 from a 20-mile round trip starting in downtown Mesa, AZ, using Halftank's own reach algorithm. Questions: Discord.