Decoding a route polyline

An encoded polyline packs a line's coordinates into a short string of text, such as _p~iF~ps|U_ulLnnqC. Routing APIs, including Mapricot's, return routes this way because it's far smaller than a list of numbers. To draw the route, decode the string back into coordinates.

Decode a polyline

Paste a polyline to see it on the map. This one is a walking route in Portland from Mapricot's routing API.

The decoder runs in your browser. Nothing you paste is sent anywhere.

Precision 5 or 6

A polyline stores each coordinate rounded to a fixed number of decimal places, called its precision. The string doesn't say which, so you have to know it from the API that made it:

Precision Used by
5 Google's Directions and Routes APIs, OSRM by default, GraphHopper
6 Mapricot, Valhalla, OSRM and Mapbox with polyline6

Most decoders default to 5. Decoding with the wrong precision moves every point by a factor of ten:

  • A precision 6 polyline decoded at 5 gives coordinates ten times too large, such as a latitude of 455 for Portland. Maps reject them, or draw nothing.
  • A precision 5 polyline decoded at 6 gives coordinates ten times too small, so a route in Portland lands in the ocean off West Africa.

HERE's routing API uses a different format, the flexible polyline, which these decoders can't read.

Decode in JavaScript

With the @mapbox/polyline package. In a web page, import it from jsDelivr in a module script:

import polyline from "https://cdn.jsdelivr.net/npm/@mapbox/polyline@1.2.1/+esm";

const points = polyline.decode(route.polyline, 6);

In Node.js, run npm install @mapbox/polyline and import it from "@mapbox/polyline" instead.

points is a list of [latitude, longitude] pairs. Leaflet takes them as they are. MapLibre and GeoJSON want [longitude, latitude], so swap them:

const coordinates = points.map(([lat, lon]) => [lon, lat]);

Routing draws a route with MapLibre this way, and Leaflet geocoding and routing with Leaflet.

Decode in Python

With the polyline package:

import polyline

points = polyline.decode(route["polyline"], 6)

points is a list of (latitude, longitude) tuples. To get GeoJSON's order, pass geojson=True.

Decode without a package

The format is simple enough to decode in a few lines. In JavaScript:

function decode(encoded, precision = 6) {
  const factor = 10 ** precision;
  const points = [];
  let index = 0;
  let lat = 0;
  let lon = 0;
  while (index < encoded.length) {
    for (const axis of ["lat", "lon"]) {
      let shift = 0;
      let value = 0;
      let byte;
      do {
        byte = encoded.charCodeAt(index++) - 63;
        value |= (byte & 0x1f) << shift;
        shift += 5;
      } while (byte >= 0x20);
      const delta = value & 1 ? ~(value >> 1) : value >> 1;
      if (axis === "lat") lat += delta;
      else lon += delta;
    }
    points.push([lat / factor, lon / factor]);
  }
  return points;
}

In Python:

def decode(encoded, precision=6):
    factor = 10**precision
    points = []
    index = lat = lon = 0
    while index < len(encoded):
        deltas = []
        for _ in range(2):
            shift = value = 0
            while True:
                byte = ord(encoded[index]) - 63
                index += 1
                value |= (byte & 0x1F) << shift
                shift += 5
                if byte < 0x20:
                    break
            deltas.append(~(value >> 1) if value & 1 else value >> 1)
        lat += deltas[0]
        lon += deltas[1]
        points.append((lat / factor, lon / factor))
    return points

Both return [latitude, longitude] pairs, the same as the packages.

How the format works

The encoding is Google's encoded polyline algorithm. For each point, it encodes the latitude and then the longitude:

  1. Multiply the value by 10 to the power of the precision, and round it to a whole number.
  2. Subtract the previous point's value, so only the change is stored. Points along a route are close together, so the changes are small numbers.
  3. Shift the number left one bit. If it was negative, invert all its bits, so the last bit says whether it's negative.
  4. Split it into 5-bit chunks, starting from the right. Set the sixth bit on every chunk but the last, to say another follows.
  5. Add 63 to each chunk, and write it as an ASCII character, from ? to ~.

Decoding runs the same steps backwards, which is what the functions above do.

Questions

Why is my decoded route in the wrong place? The precision is wrong. A route in the ocean, or ten times too far from 0, 0, was decoded at the other precision. Mapricot's routes use 6.

Why are my latitude and longitude swapped? Decoders return [latitude, longitude], but GeoJSON and MapLibre use [longitude, latitude]. Swap each pair.

Why does my polyline fail to decode? If you copied it from JSON source, each backslash in it may be doubled, as \\. Copy it from the parsed response instead, or replace each \\ with \.

How do I encode a polyline? Both packages above have encode, which takes the same precision: polyline.encode(points, 6).