Convert Between Place ID, Feature ID, and Ludocid
How to convert Google Place IDs to Feature IDs and Ludocids, with code in Python and JavaScript. Covers the protobuf encoding behind Place IDs.
Google Maps uses three different identifiers for the same place. If you've worked with the Maps API or scraped Google results, you've run into all three and wondered how they relate. They're actually different representations of the same two numbers.
This post breaks down the encoding, gives you working conversion code in Python and JavaScript, and shows how to use all three formats with the SERP Search API.
The three identifiers
Feature ID looks like 0x808fb73b6141f7b3:0x109239be3293d9ca. It's two 64-bit hex numbers separated by a colon. Google uses this internally for Maps.
Place ID looks like ChIJs_dBYTu3j4ARytmTMr45khA. It's a base64url-encoded protobuf message that contains the same two numbers as the Feature ID, just packed into a binary format.
Ludocid is a single decimal number like 1191912458399439306. It's the numeric value of the second half of the Feature ID (the part after the colon). You'll see it in Google Maps URLs as the ludocid query parameter, and it shows up in structured data and business profiles.
All three point to the same place. The differences are just encoding.
How Place ID encodes a Feature ID
A Place ID is a base64url-encoded protobuf with two fixed64 fields. Here's the byte-level breakdown for ChIJs_dBYTu3j4ARytmTMr45khA:
Base64url decode → 20 bytes:
0a 12 09 b3f74161 3bb78f80 11 cad99332 be399210
──┬── ─┬─ ────────┬─────── ─┬─ ────────┬───────
│ │ │ │ │
│ field 1 8 bytes LE │ 8 bytes LE
│ (fixed64) hi = 0x808fb73b lo = 0x109239be
│ 6141f7b3 3293d9ca
│
protobuf header (field 1, length-delimited, 18 bytes)
The two fixed64 values map directly to the Feature ID:
- hi =
0x808fb73b6141f7b3 - lo =
0x109239be3293d9ca - Feature ID =
0x808fb73b6141f7b3:0x109239be3293d9ca - Ludocid = decimal value of lo =
1191912458399439306
That's the entire encoding. No compression, no hashing, no one-way function. It's reversible.
Place ID to Feature ID (Python)
import base64
import struct
def place_id_to_feature_id(place_id: str) -> str:
"""Decode a Google Place ID into a Feature ID (hi:lo hex pair)."""
# base64url decode (pad if needed)
padded = place_id + "=" * (-len(place_id) % 4)
raw = base64.urlsafe_b64decode(padded)
# Skip the outer protobuf wrapper (first 2 bytes: field tag + length)
inner = raw[2:]
# Field 1: tag byte (0x09) + 8 bytes little-endian
hi = struct.unpack_from("<Q", inner, 1)[0]
# Field 2: tag byte (0x11) + 8 bytes little-endian
lo = struct.unpack_from("<Q", inner, 10)[0]
return f"0x{hi:x}:0x{lo:x}">>> place_id_to_feature_id("ChIJs_dBYTu3j4ARytmTMr45khA")
'0x808fb73b6141f7b3:0x109239be3293d9ca'Place ID to Feature ID (JavaScript)
function placeIdToFeatureId(placeId) {
// base64url → base64 → binary
const b64 = placeId.replace(/-/g, "+").replace(/_/g, "/");
const bin = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
// Skip 2-byte protobuf wrapper, then read two little-endian uint64s
const view = new DataView(bin.buffer, bin.byteOffset);
// Field 1 starts at offset 3 (wrapper[2] + tag[1])
const hi = view.getBigUint64(3, true);
// Field 2 starts at offset 12 (3 + 8 + tag[1])
const lo = view.getBigUint64(12, true);
return `0x${hi.toString(16)}:0x${lo.toString(16)}`;
}placeIdToFeatureId("ChIJs_dBYTu3j4ARytmTMr45khA");
// '0x808fb73b6141f7b3:0x109239be3293d9ca'Feature ID to Place ID
Going the other direction: pack the two hex values into the same protobuf structure and base64url-encode.
Python
import base64
import struct
def feature_id_to_place_id(feature_id: str) -> str:
"""Encode a Feature ID (hi:lo hex) into a Google Place ID."""
hi_str, lo_str = feature_id.split(":")
hi = int(hi_str, 16)
lo = int(lo_str, 16)
# Build protobuf: field 1 fixed64, field 2 fixed64
inner = b"\x09" + struct.pack("<Q", hi) + b"\x11" + struct.pack("<Q", lo)
# Wrap with field 1, length-delimited
outer = b"\x0a" + bytes([len(inner)]) + inner
return base64.urlsafe_b64encode(outer).rstrip(b"=").decode()>>> feature_id_to_place_id("0x808fb73b6141f7b3:0x109239be3293d9ca")
'ChIJs_dBYTu3j4ARytmTMr45khA'JavaScript
function featureIdToPlaceId(featureId) {
const [hiStr, loStr] = featureId.split(":");
const hi = BigInt(hiStr);
const lo = BigInt(loStr);
// Build protobuf bytes
const buf = new ArrayBuffer(20);
const view = new DataView(buf);
const bytes = new Uint8Array(buf);
bytes[0] = 0x0a; // outer field tag
bytes[1] = 0x12; // length = 18
bytes[2] = 0x09; // field 1 tag (fixed64)
view.setBigUint64(3, hi, true);
bytes[11] = 0x11; // field 2 tag (fixed64)
view.setBigUint64(12, lo, true);
// base64url encode (no padding)
const b64 = btoa(String.fromCharCode(...bytes));
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}featureIdToPlaceId("0x808fb73b6141f7b3:0x109239be3293d9ca");
// 'ChIJs_dBYTu3j4ARytmTMr45khA'Extracting the Ludocid
The Ludocid is just the lo value from the Feature ID, read as an unsigned 64-bit integer.
From a Feature ID
def feature_id_to_ludocid(feature_id: str) -> int:
lo_str = feature_id.split(":")[1]
return int(lo_str, 16)>>> feature_id_to_ludocid("0x808fb73b6141f7b3:0x109239be3293d9ca")
1191912458399439306From a Place ID
Chain the two functions:
>>> feature_id = place_id_to_feature_id("ChIJs_dBYTu3j4ARytmTMr45khA")
>>> feature_id_to_ludocid(feature_id)
1191912458399439306Ludocid to Feature ID
The hi half of a Feature ID doesn't matter for Google lookups. Google only uses the lo value to identify a place. That means you can convert a Ludocid into a working Feature ID by setting hi to zero:
def ludocid_to_feature_id(ludocid: int) -> str:
return f"0x0:0x{ludocid:x}">>> ludocid_to_feature_id(1191912458399439306)
'0x0:0x109239be3293d9ca'This 0x0:0x... format works in any API that accepts a Feature ID. You don't need the original hi value.
Using these with the SERP Search API
The SERP Search Place Details and Reviews endpoints accept both formats. Use feature_id for the hex format and place_id for the ChIJ format:
# Using a Feature ID
curl -G https://api.serpsearch.com/api/v1/maps/place \
-H "Authorization: Bearer YOUR_API_KEY" \
-d "feature_id=0x808fb73b6141f7b3:0x109239be3293d9ca"
# Using a Place ID
curl -G https://api.serpsearch.com/api/v1/maps/place \
-H "Authorization: Bearer YOUR_API_KEY" \
-d "place_id=ChIJs_dBYTu3j4ARytmTMr45khA"
# Using a Ludocid (zero hi, works fine)
curl -G https://api.serpsearch.com/api/v1/maps/place \
-H "Authorization: Bearer YOUR_API_KEY" \
-d "feature_id=0x0:0x109239be3293d9ca"All three return the same result. The same parameters work for the Reviews endpoint.
The Maps Search endpoint returns both place_id and feature_id in every result, so you can grab whichever format you prefer and use it directly with Place Details or Reviews.
When you'd actually need to convert
Most of the time, you won't. The API handles both formats, and if you're pulling IDs from our Maps endpoint, you already have them in both formats.
Conversion matters when you're combining data from different sources. If you have Place IDs from the official Google Maps API and Feature IDs from scraping, you need to match them up. Or if you're building a database and want a single canonical ID format. Or if you're working with legacy URLs that use the Ludocid parameter and need to look up the place.
Reference
| Format | Example | Length |
|---|---|---|
| Feature ID | 0x808fb73b6141f7b3:0x109239be3293d9ca | Variable (hex) |
| Place ID | ChIJs_dBYTu3j4ARytmTMr45khA | 27 chars (base64url) |
| Ludocid | 1191912458399439306 | Up to 20 digits |
All the code above uses only the standard library in both languages, nothing to install.
If you want to try the Place Details or Reviews endpoints, grab a free API key and check the full docs for the complete parameter reference.
Ready to get started?
Start scraping Google search results in minutes. Free tier included.