Documentation of the binary format used in the ?p= and ?m= parameters of index.html and map.php.
The ?p= parameter contains a base64url-encoded binary payload. Two format versions exist:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 byte | Header | Upper nibble = 1 (version), lower nibble = compression |
| 1 | … | Payload | Compressed or raw coordinate data |
Precision is implicitly 4 decimal places (multiplier 10000).
Example: header byte 0x10 = version 1, no compression.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 byte | Header | Upper nibble = 2 (version), lower nibble = compression |
| 1 | 1 byte | Precision | 1–4: number of decimal places |
| 2 | … | Payload | Compressed or raw coordinate data |
The multiplier is 10precision. Example: header 0x20 + precision byte 0x02 = version 2, no compression, 2 decimal places (multiplier 100).
| Value | Method |
|---|---|
0 | None |
1 | Deflate (raw) |
2 | Zlib |
3 | Gzip |
4 | BZ2 (server-side only) |
Lower precision reduces the base64url string length significantly, at the cost of coordinate accuracy:
| Precision | Multiplier | Accuracy | Typical savings (64 pts) |
|---|---|---|---|
| 1 | 10 | ~11 km | ~89% |
| 2 | 100 | ~1.1 km | ~58% |
| 3 | 1000 | ~110 m | ~38% |
| 4 | 10000 | ~11 m | (reference) |
lat_int = round(lat × 10precision)lon_int = round(lon × 10precision)delta_lat = current_lat_int - previous_lat_intdelta_lon = current_lon_int - previous_lon_int0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...zigzag(n) = (n << 1) ^ (n >> 63)
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)}")
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)}`);
5 points across Germany: Munich → Stuttgart → Frankfurt → Dortmund → Hamburg.
?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 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):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 byte | Header | Upper nibble = 3 (version), lower nibble = compression |
| 1 | 1 byte | Prec + N | Bits 7–6: precision - 1 (0–3 → 1–4 decimal places)Bits 5–0: num_paths (1–63) |
| 2 | … | Payload | Compressed or raw multi-path data |
The payload (after decompression) consists of two parts:
| Size | Field | Description |
|---|---|---|
| ceil(N×6/8) bytes | Point counts | N packed 6-bit values (0–64 points each, MSB first) |
| Then for each path (in order): | ||
| 6 bytes | First point | 2 × signed int24 (lat, lon) |
| variable | Deltas | Zigzag + varint encoded deltas for remaining points |
Example: header 0x30, byte1 0x43 = version 3, no compression, precision 2 ((1 << 6)), 3 paths (0x03).
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.
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.
Paths are assigned colors from a fixed 8-color palette, cycling if more than 8 paths are provided:
| Index | Letter | Color | Hex |
|---|---|---|---|
| 0 | a | Red | #e74c3c |
| 1 | b | Blue | #3498db |
| 2 | c | Green | #2ecc71 |
| 3 | d | Orange | #f39c12 |
| 4 | e | Purple | #9b59b6 |
| 5 | f | Teal | #1abc9c |
| 6 | g | Dark Orange | #e67e22 |
| 7 | h | Dark Gray | #34495e |
| Marker | Appearance | Popup Label |
|---|---|---|
| Start | Filled circle (radius 8), path color | a) Start |
| End | Ring (radius 8), white fill + colored border | a) Ende |
| Intermediate Hop | Small filled circle (radius 5), path color | a) Hop 1 |
3 paths from Munich to different destinations:
?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.
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}")
// 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)}`);