GPS Path Encoding Format

Documentation of the binary format used in the ?p= and ?m= parameters of index.html and map.php.

1. Format Overview

The ?p= parameter contains a base64url-encoded binary payload. Two format versions exist:

Version 1 (fixed precision: 4 decimal places)

OffsetSizeFieldDescription
01 byteHeaderUpper nibble = 1 (version), lower nibble = compression
1PayloadCompressed or raw coordinate data

Precision is implicitly 4 decimal places (multiplier 10000).

Example: header byte 0x10 = version 1, no compression.

Version 2 (variable precision: 1–4 decimal places)

OffsetSizeFieldDescription
01 byteHeaderUpper nibble = 2 (version), lower nibble = compression
11 bytePrecision14: number of decimal places
2PayloadCompressed or raw coordinate data

The multiplier is 10precision. Example: header 0x20 + precision byte 0x02 = version 2, no compression, 2 decimal places (multiplier 100).

Backward compatibility: Version 1 strings remain valid and are decoded with precision 4. Use version 2 only when a precision other than 4 is needed. The encoder automatically selects version 1 for precision=4 and version 2 for precision 1–3.

Compression Methods

ValueMethod
0None
1Deflate (raw)
2Zlib
3Gzip
4BZ2 (server-side only)

Precision & Size Trade-off

Lower precision reduces the base64url string length significantly, at the cost of coordinate accuracy:

PrecisionMultiplierAccuracyTypical savings (64 pts)
110~11 km~89%
2100~1.1 km~58%
31000~110 m~38%
410000~11 m(reference)

Payload Structure

  1. First point: 2 × signed int24 (big-endian, 3 bytes per value)
  2. Subsequent points: Delta-encoded relative to the previous point, using zigzag + varint encoding
Zigzag encoding maps signed integers to unsigned integers: 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
Formula: zigzag(n) = (n << 1) ^ (n >> 63)

2. Python Example

Complete example with no external dependencies (stdlib only). Supports version 1 (precision=4) and version 2 (precision 1–3):

import base64, struct


def zigzag_encode(n):
    return (n << 1) ^ (n >> 63)


def encode_varint(value):
    buf = bytearray()
    while value > 0x7F:
        buf.append((value & 0x7F) | 0x80)
        value >>= 7
    buf.append(value & 0x7F)
    return bytes(buf)


def encode_int24(value):
    return struct.pack(">i", value)[1:4]


def encode_path(points, precision=4):
    """Encode GPS path. precision=1..4 decimal places."""
    multiplier = 10 ** precision

    if precision == 4:
        # Version 1: backward compatible, no precision byte
        header = (1 << 4) | 0  # version=1, compression=none
        buf = bytearray([header])
    else:
        # Version 2: includes precision byte
        header = (2 << 4) | 0  # version=2, compression=none
        buf = bytearray([header, precision])

    lat0, lon0 = points[0]
    ilat = round(lat0 * multiplier)
    ilon = round(lon0 * multiplier)
    buf.extend(encode_int24(ilat))
    buf.extend(encode_int24(ilon))

    prev_lat, prev_lon = ilat, ilon
    for lat, lon in points[1:]:
        clat, clon = round(lat * multiplier), round(lon * multiplier)
        buf.extend(encode_varint(zigzag_encode(clat - prev_lat)))
        buf.extend(encode_varint(zigzag_encode(clon - prev_lon)))
        prev_lat, prev_lon = clat, clon

    return base64.urlsafe_b64encode(bytes(buf)).rstrip(b"=").decode()


# Beispiel: precision=4 (Version 1, default)
points = [(48.1372, 11.5755), (48.1380, 11.5770), (48.1395, 11.5782)]
print(f"P4: index.html?p={encode_path(points)}")

# Beispiel: precision=2 (Version 2, ~1.1 km Genauigkeit)
points_p2 = [(round(lat, 2), round(lon, 2)) for lat, lon in points]
print(f"P2: index.html?p={encode_path(points_p2, precision=2)}")


def encode_payload(points, precision, multiplier):
    """Encode points into raw payload bytes (no header)."""
    buf = bytearray()
    lat0, lon0 = points[0]
    ilat = round(lat0 * multiplier)
    ilon = round(lon0 * multiplier)
    buf.extend(encode_int24(ilat))
    buf.extend(encode_int24(ilon))
    prev_lat, prev_lon = ilat, ilon
    for lat, lon in points[1:]:
        clat, clon = round(lat * multiplier), round(lon * multiplier)
        buf.extend(encode_varint(zigzag_encode(clat - prev_lat)))
        buf.extend(encode_varint(zigzag_encode(clon - prev_lon)))
        prev_lat, prev_lon = clat, clon
    return bytes(buf)


def pack_6bit(values):
    """Pack list of 6-bit values into bytes, MSB first."""
    buf = bytearray()
    bits = nbits = 0
    for v in values:
        bits = (bits << 6) | (v & 0x3F)
        nbits += 6
        while nbits >= 8:
            nbits -= 8
            buf.append((bits >> nbits) & 0xFF)
    if nbits > 0:
        buf.append((bits << (8 - nbits)) & 0xFF)
    return bytes(buf)


def encode_multi_path(paths, precision=4):
    """Encode multiple paths into one v3 string.
    Byte 0: version=3 | compression=none
    Byte 1: (precision-1, 2 bits) | (num_paths, 6 bits)
    Then: packed 6-bit point counts + concatenated payloads."""
    if not paths:
        return ""
    if len(paths) == 1:
        return encode_path(paths[0], precision)
    multiplier = 10 ** precision
    header = (3 << 4) | 0
    byte1 = ((precision - 1) & 0x03) << 6 | (len(paths) & 0x3F)
    buf = bytearray([header, byte1])
    counts = [len(pts) for pts in paths]
    payloads = [encode_payload(pts, precision, multiplier) for pts in paths]
    buf.extend(pack_6bit(counts))
    for p in payloads:
        buf.extend(p)
    return base64.urlsafe_b64encode(bytes(buf)).rstrip(b"=").decode()


# Beispiel: Version 3 Multi-Path
path_a = [(48.14, 11.58), (49.45, 11.08), (52.52, 13.41)]
path_b = [(48.14, 11.58), (48.78, 9.18), (50.11, 8.68)]
path_c = [(48.14, 11.58), (51.34, 12.37), (53.55, 9.99)]
print(f"V3: index.html?m={encode_multi_path([path_a, path_b, path_c], precision=2)}")

3. JavaScript Example

The same implementation in JavaScript (browser-compatible), with precision support:

function zigzagEncode(n) {
    return n >= 0 ? n * 2 : (-n) * 2 - 1;
}

function encodeVarint(value) {
    const bytes = [];
    while (value > 0x7F) {
        bytes.push((value & 0x7F) | 0x80);
        value >>>= 7;
    }
    bytes.push(value & 0x7F);
    return bytes;
}

function encodeInt24(value) {
    return [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}

function encodePath(points, precision = 4) {
    const multiplier = Math.pow(10, precision);
    const buf = [];

    if (precision === 4) {
        buf.push(0x10); // version=1, compression=none
    } else {
        buf.push(0x20); // version=2, compression=none
        buf.push(precision);
    }

    let [lat0, lon0] = points[0];
    let iLat = Math.round(lat0 * multiplier);
    let iLon = Math.round(lon0 * multiplier);
    buf.push(...encodeInt24(iLat), ...encodeInt24(iLon));

    let prevLat = iLat, prevLon = iLon;
    for (let i = 1; i < points.length; i++) {
        const cLat = Math.round(points[i][0] * multiplier);
        const cLon = Math.round(points[i][1] * multiplier);
        buf.push(...encodeVarint(zigzagEncode(cLat - prevLat)));
        buf.push(...encodeVarint(zigzagEncode(cLon - prevLon)));
        prevLat = cLat;
        prevLon = cLon;
    }

    const binary = String.fromCharCode(...buf);
    return btoa(binary)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
}

// Beispiel: precision=4 (Version 1, default)
const points = [[48.1372, 11.5755], [48.1380, 11.5770], [48.1395, 11.5782]];
console.log(`P4: index.html?p=${encodePath(points)}`);

// Beispiel: precision=2 (Version 2)
const pointsP2 = points.map(([lat, lon]) =>
    [Math.round(lat * 100) / 100, Math.round(lon * 100) / 100]);
console.log(`P2: index.html?p=${encodePath(pointsP2, 2)}`);


function encodePayload(points, multiplier) {
    const buf = [];
    let [lat0, lon0] = points[0];
    let iLat = Math.round(lat0 * multiplier);
    let iLon = Math.round(lon0 * multiplier);
    buf.push(...encodeInt24(iLat), ...encodeInt24(iLon));
    let prevLat = iLat, prevLon = iLon;
    for (let i = 1; i < points.length; i++) {
        const cLat = Math.round(points[i][0] * multiplier);
        const cLon = Math.round(points[i][1] * multiplier);
        buf.push(...encodeVarint(zigzagEncode(cLat - prevLat)));
        buf.push(...encodeVarint(zigzagEncode(cLon - prevLon)));
        prevLat = cLat;
        prevLon = cLon;
    }
    return buf;
}

function pack6bit(values) {
    const buf = [];
    let bits = 0, nbits = 0;
    for (const v of values) {
        bits = (bits << 6) | (v & 0x3F);
        nbits += 6;
        while (nbits >= 8) {
            nbits -= 8;
            buf.push((bits >> nbits) & 0xFF);
        }
    }
    if (nbits > 0) buf.push((bits << (8 - nbits)) & 0xFF);
    return buf;
}

function encodeMultiPath(paths, precision = 4) {
    // Version 3: byte1 = (precision-1, 2 bits) | (num_paths, 6 bits)
    // Then packed 6-bit point counts + concatenated payloads
    if (paths.length === 0) return '';
    if (paths.length === 1) return encodePath(paths[0], precision);
    const multiplier = Math.pow(10, precision);
    const byte1 = ((precision - 1) & 0x03) << 6 | (paths.length & 0x3F);
    const buf = [0x30, byte1];
    const counts = paths.map(p => p.length);
    buf.push(...pack6bit(counts));
    for (const pts of paths) {
        buf.push(...encodePayload(pts, multiplier));
    }
    const binary = String.fromCharCode(...buf);
    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Beispiel: Version 3 Multi-Path
const pathA = [[48.14, 11.58], [49.45, 11.08], [52.52, 13.41]];
const pathB = [[48.14, 11.58], [48.78, 9.18], [50.11, 8.68]];
const pathC = [[48.14, 11.58], [51.34, 12.37], [53.55, 9.99]];
console.log(`V3: index.html?m=${encodeMultiPath([pathA, pathB, pathC], 2)}`);

4. Live Examples

5 points across Germany: Munich → Stuttgart → Frankfurt → Dortmund → Hamburg.

Version 1 – Precision 4 (default, ~11 m accuracy)

Version 2 – Precision 3 (~110 m accuracy)

Version 2 – Precision 2 (~1.1 km accuracy)

Version 2 – Precision 1 (~11 km accuracy)

Size comparison: The difference grows with more points. For 64 points, precision 1 saves ~89%, precision 2 saves ~58%, and precision 3 saves ~38% compared to precision 4 (with deflate compression).

5. Multi-Path Display (?m=)

In addition to the single-path ?p= parameter, the map supports displaying multiple paths simultaneously using the ?m= parameter. Each path is rendered in a different color with letter labels (a, b, c, …).

Version 3 – Multi-Path Format (recommended)

Version 3 encodes all paths into a single base64url string with one shared header. Precision and path count share one byte; point counts are packed at 6 bits each (max 64 points per path):

OffsetSizeFieldDescription
01 byteHeaderUpper nibble = 3 (version), lower nibble = compression
11 bytePrec + NBits 7–6: precision - 1 (0–3 → 1–4 decimal places)
Bits 5–0: num_paths (1–63)
2PayloadCompressed or raw multi-path data

The payload (after decompression) consists of two parts:

SizeFieldDescription
ceil(N×6/8) bytesPoint countsN packed 6-bit values (0–64 points each, MSB first)
Then for each path (in order):
6 bytesFirst point2 × signed int24 (lat, lon)
variableDeltasZigzag + varint encoded deltas for remaining points

Example: header 0x30, byte1 0x43 = version 3, no compression, precision 2 ((1 << 6)), 3 paths (0x03).

Bit packing: Precision (2 bits) and path count (6 bits) share one byte. Point counts use 6 bits each (max 64 points per path) packed MSB-first into ceil(N×6/8) bytes. For 8 paths this saves 3 bytes vs 8-bit counts, resulting in ~4 fewer base64url characters in the URL.

Legacy Format (backward compatible)

The legacy format joins multiple base64url-encoded path segments with ~ as delimiter:

index.html?m=<path_a>~<path_b>~<path_c>

Each segment uses the same binary encoding format as ?p= (version 1 or 2). The decoder auto-detects: if the ?m= value contains ~, it uses the legacy tilde-split approach. Otherwise, it decodes as a single v3 multi-path string.

Color Palette

Paths are assigned colors from a fixed 8-color palette, cycling if more than 8 paths are provided:

IndexLetterColorHex
0aRed#e74c3c
1bBlue#3498db
2cGreen#2ecc71
3dOrange#f39c12
4ePurple#9b59b6
5fTeal#1abc9c
6gDark Orange#e67e22
7hDark Gray#34495e

Marker Types

MarkerAppearancePopup Label
StartFilled circle (radius 8), path colora) Start
EndRing (radius 8), white fill + colored bordera) Ende
Intermediate HopSmall filled circle (radius 5), path colora) Hop 1

Multi-Path Live Examples

3 paths from Munich to different destinations:

Version 3 Multi-Path (recommended)

Precision 4 (~11 m accuracy)
Precision 3 (~110 m accuracy)
Precision 2 (~1.1 km accuracy, default for multitest bot)
Precision 1 (~11 km accuracy)

Legacy tilde-separated (still supported)

Precision trade-off for multi-path: Precision 2 (~1.1 km) is the default for the multitest bot — a good balance between accuracy and URL length for mesh network paths. The precision is configurable per bot in the Bot Management settings.
Backward compatibility: The ?p= parameter continues to work unchanged for single paths. When both ?m= and ?p= are present, ?m= takes precedence. Legacy tilde-separated ?m= URLs remain fully supported.

Python: Generating Multi-Path URLs

from path_encoder import encode_multi_path

# Define paths
path_a = [(48.1372, 11.5755), (49.4521, 11.0767), (52.5200, 13.4050)]
path_b = [(48.1372, 11.5755), (48.7758, 9.1829), (50.1109, 8.6821)]
path_c = [(48.1372, 11.5755), (51.3397, 12.3731), (53.5511, 9.9937)]

# v3 multi-path: single string, one header, point-count per path
encoded = encode_multi_path([path_a, path_b, path_c], precision=2)
print(f"index.html?m={encoded}")

JavaScript: Generating Multi-Path URLs

// v3 multi-path encoding
function encodeMultiPath(paths, precision = 4) {
    if (paths.length === 0) return '';
    if (paths.length === 1) return encodePath(paths[0], precision);

    const multiplier = Math.pow(10, precision);
    const buf = [];
    buf.push(0x30);        // version=3, compression=none
    buf.push(precision);   // precision byte

    for (const points of paths) {
        buf.push(points.length & 0xFF);  // point count

        let [lat0, lon0] = points[0];
        let iLat = Math.round(lat0 * multiplier);
        let iLon = Math.round(lon0 * multiplier);
        buf.push(...encodeInt24(iLat), ...encodeInt24(iLon));

        let prevLat = iLat, prevLon = iLon;
        for (let i = 1; i < points.length; i++) {
            const cLat = Math.round(points[i][0] * multiplier);
            const cLon = Math.round(points[i][1] * multiplier);
            buf.push(...encodeVarint(zigzagEncode(cLat - prevLat)));
            buf.push(...encodeVarint(zigzagEncode(cLon - prevLon)));
            prevLat = cLat;
            prevLon = cLon;
        }
    }

    const binary = String.fromCharCode(...buf);
    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

const pathA = [[48.14, 11.58], [49.45, 11.08], [52.52, 13.41]];
const pathB = [[48.14, 11.58], [48.78, 9.18], [50.11, 8.68]];
const pathC = [[48.14, 11.58], [51.34, 12.37], [53.55, 9.99]];

console.log(`index.html?m=${encodeMultiPath([pathA, pathB, pathC], 2)}`);