Skip to content

Support maxZoom >= 31 without corrupting cluster ids - #262

Open
spokodev wants to merge 1 commit into
mapbox:mainfrom
spokodev:fix/maxzoom-31-cluster-id-overflow
Open

Support maxZoom >= 31 without corrupting cluster ids#262
spokodev wants to merge 1 commit into
mapbox:mainfrom
spokodev:fix/maxzoom-31-cluster-id-overflow

Conversation

@spokodev

Copy link
Copy Markdown

Problem

The cluster id packs the origin zoom into the low 5 bits:

const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;   // encode
_getOriginId(id)   { return (id - this.points.length) >> 5; }
_getOriginZoom(id) { return (id - this.points.length) % 32; }

Five bits hold zoom + 1 only up to 31, i.e. maxZoom <= 30. Two coincident
(or Float32-coincident) points always cluster at zoom-pass = maxZoom, so with
maxZoom >= 31 the zoom + 1 value (>= 32) overflows the zoom field into the
origin-index bits. Decoding then returns the wrong origin id/zoom, and
getChildren / getLeaves / getClusterExpansionZoom throw
"No cluster with the specified id." on input the library silently accepts —
maxZoom has no documented upper bound (README: default 16).

const index = new Supercluster({maxZoom: 31}).load([
  {type: 'Feature', properties: {}, geometry: {type: 'Point', coordinates: [10, 50]}},
  {type: 'Feature', properties: {}, geometry: {type: 'Point', coordinates: [10, 50]}},
]);
const id = index.getClusters([-180, -90, 180, 90], 0).find(f => f.properties.cluster).properties.cluster_id;
index.getLeaves(id); // throws: No cluster with the specified id.

This is the symptom in #221 (maxZoom: 999 → "originId is too high") and #243
("behavior strange when zoom above 20").

Fix

Size the zoom field to the configured maxZoomzoomBase, the smallest power
of two >= 32 that can hold maxZoom + 1 — and pack/unpack with * / / / %
instead of the fixed 5-bit shifts. zoomBase === 32 for every maxZoom <= 30,
so existing cluster ids are byte-identical (the current tests, which assert
hardcoded ids 164/196/581/2504, still pass unchanged). Using arithmetic instead
of the 32-bit bitwise operators also removes a latent >> overflow for very
large point counts.

Test

Added a case asserting getLeaves / getChildren / getClusterExpansionZoom
work for maxZoom of 31, 32 and 40. Fails on main ("No cluster with the
specified id."), passes with the fix; the existing 15 tests are unchanged.

Fixes #221, #243.

The cluster id packs the origin zoom into the low 5 bits (`<< 5`, `>> 5`,
`% 32`), which can only hold `zoom + 1` up to 31 — i.e. maxZoom <= 30.
Two coincident (or Float32-coincident) points always cluster at
zoom-pass = maxZoom, so with `maxZoom >= 31` the `zoom + 1` value (>= 32)
overflows the zoom field into the origin-index bits. Decoding then yields
the wrong origin id and zoom, and `getChildren` / `getLeaves` /
`getClusterExpansionZoom` throw "No cluster with the specified id." on
input the library silently accepts (there is no documented upper bound on
maxZoom).

Size the zoom field to the configured maxZoom (`zoomBase`, a power of two
>= 32) and pack/unpack with `*` / `/` / `%` instead of the fixed 5-bit
shifts. `zoomBase === 32` for every maxZoom <= 30, so existing cluster ids
are unchanged; using arithmetic instead of the 32-bit bitwise operators
also removes a latent overflow for very large point counts.

Fixes mapbox#221, mapbox#243.
@spokodev
spokodev requested a review from a team as a code owner July 23, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No cluster with the specified id thrown (originId is too high)

1 participant