picking-and-hit-testing · git:20261026.79a684e · 2026-10-26 · sha256 7d62a23aa0f86a65

picking-and-hit-testing git:20261026.79a684eA

Immutable. This exact content never changes and is served at /api/v1/blob/7d62a23aa0f86a65.

---
name: picking-and-hit-testing
description: How map engines answer "what is under the pointer" (GPU id buffers, Canvas hit canvases, CPU indexes and analytic tests, precomputed id grids, terrain raycasts), with the stalls, id limits, tolerance bugs and cross-tile identity problems they measured and reverted. Load it when a map-from-scratch request needs tap, click or hover to identify, highlight or select features, a coordinate or height under the cursor, or "everything under the finger", before writing `architecture_selection` or any bake or renderer code for interaction, or when picks are slow, stall frames, miss thin lines or return the wrong feature.
---

# Picking and hit testing: what is under the pointer, and how to ask

A pick turns a screen position into a feature id (or a ground coordinate),
and the id into the attributes the user asked for. A general engine must
pick any geometry, styled any way, moved by any shader, from any number of
layers and tiles. Most of them therefore render the scene a second time with
ids as colours and read the pixel back, and then spend years on what that
costs: GPU stalls, id-range limits, antialiased ids, picks that disagree with
what was drawn, and features split across tiles. A renderer made for one
request knows what is pickable, how it is drawn and how many there are, and
can often answer from data it already holds on the CPU. The repo has both
ends already: a 1x1 id render on a phone (02, pick p95 0.9 ms at 4x CPU
throttle) [1], a brute-force CPU hover over 7,342 cities (07, p95
0.25-0.3 ms, no index) [2], and `isPointInPath` over 47 prefectures (01,
p95 0.01 ms) [3].

Per-engine detail and the smaller related decisions are in
[references/engines.md](references/engines.md). Numbers in brackets refer to
the Sources list at the end of this file.

## First question: which mechanism answers the pick?

| The request | Answer the pick with | Engines that show why |
| --- | --- | --- |
| Up to ~10^4 simple shapes (points of known radius, lines of known width, polygons) whose geometry the CPU already holds | CPU: bbox or grid prefilter, then an exact test in the space and with the fill rule the renderer draws with | galileo [4], Terria [5][6], Leaflet [7][8], 07 [2] |
| Height or ground coordinate under the cursor over terrain | CPU raycast against the DEM the renderer already holds | MapLibre [9][10][11], Cesium [12] |
| Dense or overlapping GPU geometry whose final shape the shader decides (instancing, line quads, extrusion, draping), or arbitrary icons | GPU id buffer: ids into a tiny target at the pointer, read one pixel, asynchronously where possible | deck.gl [13], navara [14], WorldWind [15], tangram [16], 02 [1] |
| Canvas 2D with a few dozen paths | `isPointInPath`, no index; per-feature hit canvases are slow | 01 [3], OpenLayers [17][18] |
| Geometry not shipped to the client (raster tiles), but hover must name features | A precomputed id grid per tile (UTFGrid), baked with the tiles | Mapnik [19], ol2 [20][21], TileMill [22] |
| Labels drawn by a placement pass | Query the placement result in screen space, not the source features | MapLibre [23], OpenLayers [24] |

Where the geometry is already on the CPU, the engines moved away from
synchronous GPU readback: MapLibre deleted its terrain coordinate framebuffer
for a CPU raycast [10][25] and its per-marker depth readback
for a CPU ray walk [11]; OpenLayers moved picks from composited screen
pixels to each layer's own data [26]. They kept GPU picking where the
shader decides the final shape: navara picks text and terrain-deformed
geometry from an id buffer [14], and deck.gl's overlapping-pick RFC set CPU
ray casting aside because instanced layers define their final geometry in the
vertex shader, so each layer could need custom JS to match it, and the GLSL and
JS could get out of sync [84].

## Decisions, with trade-offs and evidence

### The synchronous readback is the cost, not the id render

- MapLibre terrain picking re-rendered every terrain tile into a
  screen-sized framebuffer with a 4 MB coordinate texture on every pointer
  event; positions were quantised to 1024 steps per tile and a 1-byte tile
  index broke past 255 tiles [10][25]. The CPU replacement
  marches the ray, bisects the first crossing and refines with a secant step
  (on the globe it brackets between min- and max-elevation spheres). It costs
  0.2 us for a sky miss up to 63 us for a sloped hit at z16; picks differ by
  ~0.5 px median (p95 <= 1.6 px, inside the old quantisation), and globe
  unproject went from ~12 px off to 0.00 px [10]. The changelog sums it
  up as full DEM resolution, no GPU stall and ~4 MB less GPU memory [9].
- MapLibre marker occlusion read one depth pixel per marker every 100 ms:
  45 markers on a Galaxy S24 turned a 33 ms worst frame into 108-117 ms
  stalls, and desktop panning fell from 87 to 47-55 fps. The CPU ray walk
  costs 5-10 us per marker at pitch 30-60 and 75-90 us at pitch 85, agreed
  with the depth method on every on-screen marker, and the demo counted zero
  `readPixels` [11].
- Cesium measured synchronous `readPixels` at 46% of total execution time on
  Intel Arc, sometimes longer than a whole frame. `pickAsync` reads into a
  pixel-pack buffer and waits on a fence, so results arrive a frame or more
  later; the synchronous API stayed for callers that need an answer now
  [27]. openglobus did the same and never starts a read while one is
  pending [28]; tangram defers `readPixels` with a timer because the
  synchronous call was "a massive performance hit" [16].
- Read once, answer many: Cesium caches `pickPosition` per frame
  [29]; tangram-es reuses the colour read at the same pixel within a
  frame [30]; iTowns reads the whole depth buffer once while the view
  is idle, answers hover picks from the array, and drops it on the next
  render [31][32].

### If you render ids, render them correctly

- Tiny target: navara and WorldWind draw pickables into a 1x1 (or small
  pick-rectangle) viewport at the cursor [14][15]; 02 does it by
  moving the camera centre to the tap [1]. Any shader that samples a
  screen-space texture then sees `gl_FragCoord` = (0.5, 0.5): navara's ground
  polylines sampled depth at (0, 0) and had to be clicked at a visibly
  different place, until the real pick pixel was passed as a uniform
  [33].
- No antialiasing in the id target. Averaged edge colours decode to ids that
  do not exist, so navara skips MSAA for pick bakes [14]; ol2's Canvas
  hit canvas could not turn antialiasing off, so it ignored pixels with alpha
  != 255 and wrapped decoding in try/catch [34][35]. OpenLayers
  made its hit canvas opaque (`alpha: false`) and decodes RGB only
  [36]. Sample id textures with nearest filtering: linear filtering
  blended neighbouring bitmasks at edges in navara [37].
- Id capacity: 24-bit RGB gives 16,777,215 ids. deck.gl turned a hard assert
  into a warning and capped its colour cache at 2^24-1 [38]; it puts the
  layer index in alpha through the blend constant (limit 256 pickable
  layers), which overflowed with many tiled sublayers until alpha values were
  assigned only to the (layer, viewport) pairs drawn in that pass
  [39][40]. Mapnik's grid needed 64-bit ids for OSM
  [19]. Remap to dense ids at bake time instead.
- Store fewer id bytes: deck.gl 9.4 derives the pick colour from the instance
  index and keeps an explicit `rowIndexes` attribute only where one feature
  becomes many instances (path segments, MVT sublayers), "reducing memory
  usage and initialization times" [41]. OpenLayers packs the id colour
  into 2 float attributes instead of 4; the first unpack function was not
  exact and returned wrong ids [42].
- Resolution: OpenLayers renders its WebGL hit pass at half width and height,
  a 4x smaller allocation "especially for retina devices" [43], and its
  Canvas hit detection at pixel ratio 1 [36].
- The pick pass must match what the user sees. tangram-es rejected skipping
  non-selectable features in the selection buffer, because hidden features
  then became pickable; occluders are drawn as a null id [44].
  WorldWind discards fully transparent icon texels in the pick pass and
  widens symbol lines to 9 px there [45]. Thin GL lines aliased out of
  osgEarth's small pick texture; the fix was drawing lines as quads, not a
  bigger target [46].
- Only when needed: tangram creates the selection framebuffer lazily and
  skips the pass when nothing is interactive [47]; deck.gl skips
  picking when no layer is pickable and visible [48], and during a pick
  draws only tiles that intersect the pick rectangle [49]. One shared
  target, not one per layer: deck.gl dropped per-layer picking framebuffers
  after apps with 80+ layers had performance problems [13].

### If you test on the CPU, test what was drawn

- Prefilter, then test exactly. galileo checks a cached bbox before the
  point-in-feature test and skips tiles that do not contain the point, which
  "saves a lot of computational power" [4]; Terria caches each
  feature's bbox and runs the exact intersection only on bbox hits
  [5]; Leaflet tests only layers that survived viewport culling
  [8]; MapLibre Native discards invisible layers before the query
  instead of after, which made its query benchmark almost 2x faster
  [50].
- Index static data once, in bulk. OpenLayers replaced its own R-tree
  (recurring remove and infinite-loop bugs) with rbush and bulk insert, and
  clearing 100,000 features became about 3 s faster, ~350 ms [51]. For incremental R-trees, GRASS chooses nodes by
  bounding-sphere volume, because flat boxes of axis-aligned data have zero
  volume and attract distant entries [52]. Each feature belongs in
  one tile's index; query the neighbouring tiles for features that cross the
  seam [53].
- Same space, same rule as the renderer. Leaflet reviewers required
  point-in-polygon on projected points with the even-odd rule its Canvas
  renderer uses [7]; Terria reported a hit in both ACT and NSW until
  holes were honoured by ring order and winding sign [6]; osgEarth
  samples heights on the same quad diagonal the mesh uses [54]; d3-geo
  answers "outside" for a polygon boundary through the pole, an arbitrary
  choice made because the other answer broke cutting [55].
- The symbol is the hit area, not the anchor. MapServer tests clicks against
  the rendered symbol footprint and its anchor point, because a square
  tolerance gave false hits and misses for rectangular and bottom-anchored
  symbols [56]; GeoServer insists that picks use what was depicted,
  since large or offset markers spill outside the anchor geometry
  [57]. OpenLayers kept render-based hit detection for this reason: a
  geometry index does not know stroke widths and symbol sizes [17]. A
  baked renderer does know them; 07 keeps the baked radius for both the
  shader and the CPU test [2].
- Batching and rasterising lose identity. WorldWind composites surface shapes
  into textures but picks each shape on its own [58]; iTowns needed
  CPU geometry tests for vectors drawn into terrain textures [59];
  globe.gl's merged meshes lost per-datum hover and at first swallowed clicks
  meant for the globe beneath [60].
- Heavy meshes: Cesium builds a terrain-pick quadtree for a tile only when
  that tile is first picked, in a worker, and uses the default pick until it
  is ready [12].

### Tolerance: CSS pixels, scaled once, nearest first

- OpenLayers' `hitTolerance` is in CSS px; a 6.5 fix removed a second
  multiplication by device pixel ratio [61]. Before tolerance
  existed, tapping lines and points under 10 px was "a matter of luck"; it was
  implemented as a cached circle mask [62], later reordered so the
  closest pixels are checked first, with an early exit [61].
- Pixels, not metres: Organic Maps replaced a 5 m snap radius with 10 px,
  because metres were too strong at high zoom and too weak at low zoom;
  reviewers also caught the first match in the tap rectangle winning over the
  nearest one [63]. GRASS's click query returned no lines until the
  distance threshold followed the current map extent [64].
- Tolerance for point picks only: MapServer's 3 px default for point and line
  layers selected points outside box and polygon queries and made counts
  depend on image resolution [65]. GeoServer derives the buffer from
  the largest symbolizer, at least 5 px [66].
- Tolerance per layer: globe.gl's line threshold also applied to polygon
  outline strokes, so small polygons between two others could not be hovered
  [67]. Leaflet made tolerance a renderer option instead of a
  touch-only constant [68], and keeps raw pixel bounds apart from the
  tolerance-padded ones so a style change does not reproject [69].
- One pixel space at the input boundary: WorldWind Java switched every event
  and pick coordinate to device pixels after scaled displays broke picking,
  and missed call sites kept surfacing afterwards [70].

### Hover scheduling

- At most one pick per frame, none while dragging, none without a listener:
  navara [14], deck.gl [48], Leaflet [8][71],
  openglobus [28], Terria [72]. Leaflet's throttle lost
  mouseover and mouseout on fast in-and-out moves until leave events bypassed
  it [71]. Terria copies pointer positions when it defers work, so
  screen, scene and globe positions describe one instant [72].
- One listener on the container, not one per feature: Leaflet's event
  delegation rewrite improved layer add/remove performance and memory
  footprint; an earlier, separate event propagation rewrite had made
  FeatureGroup and GeoJSON adding and events about 10x faster [73].
- Profile before blaming the hit test: OpenLayers hover slowness turned out to
  be re-filling every polygon on each style change; a highlight should redraw
  an overlay, not the base layer [74].

### Identity: a pick must resolve to a stable feature id

- Features split by tiles: deck.gl highlighted only the hovered tile's piece
  until it kept the feature id and highlighted that id in every tile
  [75]; navara stores the selected id and applies it to batches that
  load later [76]; reearth keeps id -> [(tile content, batchId)], built
  as tiles load [77].
- Where ids come from: Terria's region picks were off by one until feature
  ids were frozen in the source data [78]; QGIS ignores MVT feature ids
  because real mbtiles reuse them across tiles [79]; TileMill requires
  a key only on the one interactive layer [22].
- Labels: one pick id per label, not per feature [80].
- Attributes on demand: maptalks ships feature JSON from the worker as one
  transferable buffer and decodes it only when queried [81]; iTowns
  stopped building per-feature batch-table objects at tile load [82].
  ortho-earth's fid-indexed style table coloured the neighbour after a bake
  step compacted broken features and shifted every fid [83].

### Everything under the finger

- deck.gl repeats the id pass up to depth 10, zeroing the previous hit's
  colour each time; reviewers required this only on click, never on hover,
  because the passes and readbacks dominate the cost [84][85].
  Cesium's drill pick moved to breadth-first with a limit, ordered by distance
  from the centre [86]. ol2's Canvas renderer could only report the
  topmost feature [34]. A CPU test can return every candidate from
  one scan.

### Precomputed id grids

- Mapnik's grid renderer burns ids into a per-tile grid [19]; UTFGrid
  defaults to 2x2 px cells, a quarter of the data [21]; ids encoded as
  characters fail in the UTF-16 surrogate range [20]; lookups should be
  synchronous and return null until the grid tile has loaded, with grids
  preloaded alongside the visible tiles [87].

## Tried and reverted

- A GPU coordinate framebuffer for terrain picks, and per-marker depth
  readback; both replaced by CPU rays (MapLibre [10][11]).
- `willReadFrequently` on the Canvas hit context: set in 2022; removed in
  2025-03 ("Hint willReadFrequently leads to worse performance when selecting
  features"); reintroduced in 2025-04 to prevent a Canvas2D warning; removed
  again in 2025-06; then chosen at the first hit test in 2025-11 by a 50 ms
  micro-benchmark of each of true, false and undefined, keeping the fastest
  (OpenLayers [18]).
- An UnsignedByte id attachment to save VRAM, reverted because MRT
  attachments must share one type (navara [37]).
- Per-layer picking framebuffers, and a hard assert at 2^24 objects (deck.gl
  [13][38]); deep-pick attribute rewrites that the first PR ran on
  every pointer move (deck.gl [85]).
- PostGIS key autodetection for UTFGrid ids, rolled back because it broke too
  many stylesheets (TileMill [22]).
- A 5 m building snap radius, replaced by pixels (Organic Maps [63]).
- Per-OS HiDPI multipliers (screenResolution/96), replaced by one
  device-pixel space (WorldWind Java [70]).

## Guidance for a renderer in this repo

Write into `architecture_selection.not_doing` what the request does not need:
hover (click or tap only), highlight, picking at all, several features per
pick, coordinates or heights under the cursor, labels as pick targets. A map
without interaction needs no pick pass and no index [47][48].

Choose the mechanism:

1. **CPU first when the bake knows the shapes.** Points with baked radii,
   lines with known widths, polygons with rings: keep positions in a typed
   array, test topmost-first in draw order, in the renderer's projected
   space, with the same radius and fill rule as the drawing [7][6].
   A linear scan over ~10^4 points stays well under a millisecond [2];
   add a uniform grid or a bulk-loaded packed R-tree baked into the file only
   when the bench says so [51][52]. Nothing comes back from
   the GPU.
2. **Id buffer when the shader decides the shape** (instanced line quads,
   extrusion, draping, many overlapping polygons, as in 02): 1x1 or small
   target at the pointer [14][15], no MSAA, opaque, nearest
   [37][36], ids from `gl_VertexID`/`gl_InstanceID` or a baked
   dense id [41], clear to 0 = none, render only on a pick request, at
   most once per frame, never while dragging [48][47]. On WebGL2
   read through a PBO and a fence when a frame of delay is acceptable
   [27][28]; for a tap, one synchronous 1x1 read was 0.9 ms
   p95 in 02 [1]. Pass the real pick pixel to any shader that samples
   screen-space buffers [33]. Draw lines into the pick pass a few pixels
   wide [46][45].
3. **Terrain**: raycast the baked heightfield on the CPU [10], and
   sample it with the mesh's own triangulation [54].

Bake for identity: one dense integer id per feature, written once into the
geometry and the attribute table, stable across tiles and LOD
[75][78][79]; attributes in a columnar side table decoded
on pick [81][82]. Tolerance: CSS px from the spec, times DPR
once at the input boundary [61][70], nearest candidate first
[63], none for region selection [65].

Budgets and checks to add to `spec.yaml`:

- `pick_ms` or `hover_ms` p95, including the readback and the UI update (02,
  07), and a frame-time budget measured while hovering, so that a stall shows
  up as a frame and not only as pick time [11][27].
- Reachability: for every feature (or a uniform sample), put it under the
  pointer and check the pick returns its id (07's `reachable_ratio` >= 0.99)
  [2]; take the positions from the baked data, not by hand [88].
- Negative picks: a point over sea or background returns none.
- If the data is tiled, a feature cut by a tile seam returns the same id on
  both sides [75][53].
- One pick check at DPR 1 and one at DPR 2 or 3, to catch tolerance or
  coordinate scaling applied twice or not at all [61][70].

## Sources

1. examples/02-world-population-mobile/spec.yaml (performance_knowledge id-pick-one-pixel), skills/map-from-scratch/references/patterns.md (Interaction)
2. examples/07-world-cities-hover/spec.yaml (brute-force-hover, reachable_ratio), examples/07-world-cities-hover/cities.ts (pick)
3. examples/01-prefecture-picker/spec.yaml (performance_knowledge no-index-needed)
4. https://github.com/galileo-map/galileo/commit/cd61976, https://github.com/galileo-map/galileo/commit/3196f9e
5. https://github.com/TerriaJS/terriajs/commit/06cfa3a19
6. https://github.com/TerriaJS/terriajs/commit/f44043366
7. https://github.com/Leaflet/Leaflet/pull/4618
8. https://github.com/Leaflet/Leaflet/commit/fd64c7e5
9. ref-repos/maplibre__maplibre-gl-js/CHANGELOG.md (6.6.0 #7640, 6.10.0 #8387), https://github.com/maplibre/maplibre-gl-js/issues/7640
10. https://github.com/maplibre/maplibre-gl-js/pull/8158, https://github.com/maplibre/maplibre-gl-js/issues/1596
11. https://github.com/maplibre/maplibre-gl-js/pull/8387
12. https://github.com/CesiumGS/cesium/commit/53fa96bf858d13a2c2b0b054c775459d9346cdc1, https://github.com/CesiumGS/cesium/commit/1e8d248e34cc84a11aa512e2a09321c59cff1b39, https://github.com/CesiumGS/cesium/commit/debd74eb915aed2d3e09eb73a991f4c2c9f907e1
13. https://github.com/visgl/deck.gl/blob/master/dev-docs/RFCs/v4.1/picking-improvements-rfc.md, https://github.com/visgl/deck.gl/blob/master/docs/developer-guide/performance.md, ref-repos/visgl__deck.gl/dev-docs/roadmaps/performance-roadmap.md
14. https://github.com/maplibre/navara/blob/main/guide/ARCHITECTURE.md, https://github.com/maplibre/navara/blob/main/guide/TILE_TEXTURE_COMPOSITING.md
15. ref-repos/NASAWorldWind__WorldWindJava/src/overview.html, ref-repos/NASAWorldWind__WorldWindJava/src/gov/nasa/worldwind/render/DrawContextImpl.java (https://github.com/NASAWorldWind/WorldWindJava/blob/2543c23/src/gov/nasa/worldwind/render/DrawContextImpl.java), ref-repos/NASAWorldWind__WorldWindJava/src/gov/nasa/worldwind/BasicSceneController.java
16. https://github.com/tangrams/tangram/blob/master/src/selection/selection.js (local: ref-repos/tangrams__tangram/src/selection/selection.js)
17. https://github.com/openlayers/openlayers/issues/4232, https://github.com/openlayers/openlayers/issues/4232#issuecomment-232302739
18. https://github.com/openlayers/openlayers/commit/b75d4e8ef, https://github.com/openlayers/openlayers/commit/1eb51f456, https://github.com/openlayers/openlayers/commit/295a5b77a, https://github.com/openlayers/openlayers/commit/cf24c77e9, https://github.com/openlayers/openlayers/commit/4ce546b4a, https://github.com/openlayers/openlayers/commit/8f6f87b85
19. https://github.com/mapnik/mapnik/blob/master/CHANGELOG.md, https://github.com/mapnik/mapnik/issues/1661, https://github.com/mapnik/mapnik/issues/1962, ref-repos/mapnik__mapnik/docs/design.md
20. https://github.com/openlayers/ol2/pull/244
21. https://github.com/openlayers/ol2/commit/89e8a872, https://github.com/openlayers/ol2/commit/80656cdf
22. https://github.com/tilemill-project/tilemill/issues/1358, ref-repos/tilemill-project__tilemill (commit 3008d9ca), ref-repos/tilemill-project__tilemill/_posts/docs/reference/0204-03-01-project-settings.md
23. https://github.com/maplibre/maplibre-gl-js/commit/fc992ec59
24. https://github.com/openlayers/openlayers/commit/08af20772, https://github.com/openlayers/openlayers/commit/80e67bac7, https://github.com/openlayers/openlayers/commit/ac195c46b, https://github.com/openlayers/openlayers/commit/a2d41da30
25. https://github.com/maplibre/maplibre-gl-js/commit/8c274a9cd, https://github.com/maplibre/maplibre-gl-js/commit/17ae8e33c
26. ref-repos/openlayers__openlayers/changelog/upgrade-notes.md, ref-repos/openlayers__openlayers/changelog/v6.1.0.md, ref-repos/openlayers__openlayers/changelog/v6.13.0.md
27. https://github.com/CesiumGS/cesium/pull/12983
28. https://github.com/openglobus/openglobus/commit/6e17c9a2, https://github.com/openglobus/openglobus/commit/b52eed53, https://github.com/openglobus/openglobus/commit/13c97f3d, https://github.com/openglobus/openglobus/commit/b1b2f38b
29. ref-repos/CesiumGS__cesium/CHANGES.md, https://github.com/CesiumGS/cesium/issues/5117
30. https://github.com/tangrams/tangram-es/commit/1e3ce2de
31. https://github.com/iTowns/itowns/commit/061d19e3
32. https://github.com/iTowns/itowns/commit/3a8daf03, https://github.com/iTowns/itowns/commit/ac2e8000
33. https://github.com/maplibre/navara/pull/420
34. https://github.com/openlayers/ol2/blob/master/lib/OpenLayers/Renderer/Canvas.js, https://github.com/openlayers/ol2/blob/master/notes/2.13.md
35. https://github.com/openlayers/ol2/commit/3ef0b059, https://github.com/openlayers/ol2/commit/11191464
36. https://github.com/openlayers/openlayers/commit/4b13c6dae, https://github.com/openlayers/openlayers/commit/ac5f52a21, https://github.com/openlayers/openlayers/commit/315695eeb
37. https://github.com/maplibre/navara/commit/f96c2449
38. https://github.com/visgl/deck.gl/commit/37237d01
39. https://github.com/visgl/deck.gl/commit/0cff9773, https://github.com/visgl/deck.gl/commit/b8c7d33a, ref-repos/visgl__deck.gl/docs/developer-guide/performance.md ("Picking limitations"; docs/advanced/performance.md at 43fd968b)
40. https://github.com/visgl/deck.gl/commit/08e442f4, https://github.com/visgl/deck.gl/commit/80f4c7c1
41. https://github.com/visgl/deck.gl/blob/master/docs/whats-new.md, https://github.com/visgl/deck.gl/blob/master/docs/upgrade-guide.md, https://github.com/visgl/deck.gl/commit/0c4b7d11, https://github.com/visgl/deck.gl/commit/48760d53
42. https://github.com/openlayers/openlayers/commit/a4b7f7062
43. https://github.com/openlayers/openlayers/commit/600e1a464
44. https://github.com/tangrams/tangram-es/pull/948
45. ref-repos/NASAWorldWind__WorldWindJava/src/gov/nasa/worldwind/symbology/AbstractTacticalSymbol.java (https://github.com/NASAWorldWind/WorldWindJava/blob/2543c23/src/gov/nasa/worldwind/symbology/AbstractTacticalSymbol.java)
46. https://github.com/pelicanmapping/osgearth/issues/1062
47. https://github.com/tangrams/tangram/commit/068aec9c, https://github.com/tangrams/tangram/commit/7c515884
48. https://github.com/visgl/deck.gl/commit/08c389d8, https://github.com/visgl/deck.gl/commit/173a43ea
49. https://github.com/visgl/deck.gl/commit/13bed5d1
50. https://github.com/maplibre/maplibre-native/commit/0051fcdbb85b2a02751c62d1c76ee1518a967bcb
51. https://github.com/openlayers/openlayers/commit/af4184e74, https://github.com/openlayers/openlayers/commit/97acd989a, https://github.com/openlayers/openlayers/commit/17e56d835, https://github.com/openlayers/openlayers/commit/3e2cc3c24
52. ref-repos/OSGeo__grass/lib/vector/rtree/docs/README.txt, https://github.com/OSGeo/grass/blob/main/lib/vector/rtree/docs/README.txt
53. https://github.com/maplibre/maplibre-native/commit/90f6c531f31a4edc5945f15a9864b914ce156ba8
54. https://github.com/pelicanmapping/osgearth/commit/a1663895ca8ac6650cdf8aed097951c536846950, https://github.com/pelicanmapping/osgearth/commit/b0fd185975425941205772e6996f01b35fd200ca
55. https://github.com/d3/d3-geo/commit/aff9146, https://github.com/d3/d3-geo/issues/105
56. https://github.com/MapServer/MapServer/commit/b8f298d7, https://github.com/MapServer/MapServer/commit/834b621d
57. https://github.com/geoserver/geoserver/pull/3961
58. ref-repos/NASAWorldWind__WorldWindJava/src/gov/nasa/worldwind/render/SurfaceObjectTileBuilder.java (https://github.com/NASAWorldWind/WorldWindJava/blob/2543c23/src/gov/nasa/worldwind/render/SurfaceObjectTileBuilder.java), ref-repos/NASAWorldWind__WorldWindJava/src/gov/nasa/worldwind/AbstractSceneController.java
59. https://github.com/iTowns/itowns/pull/450, https://github.com/iTowns/itowns/pull/470
60. https://github.com/vasturiano/globe.gl/issues/223, https://github.com/vasturiano/globe.gl/commit/97541ca, https://github.com/vasturiano/globe.gl/commit/5c9b91c, https://github.com/vasturiano/globe.gl/commit/0f2844c, ref-repos/vasturiano__globe.gl/README.md
61. https://github.com/openlayers/openlayers/commit/c076d273e, ref-repos/openlayers__openlayers/changelog/upgrade-notes.md (6.5)
62. https://github.com/openlayers/openlayers/issues/5862, https://github.com/openlayers/openlayers/pull/5995
63. https://github.com/organicmaps/organicmaps/pull/7876
64. https://github.com/OSGeo/grass/pull/3793, https://github.com/OSGeo/grass/blob/main/gui/wxpython/mapdisp/frame.py
65. https://github.com/MapServer/MapServer/issues/1103
66. ref-repos/geoserver__geoserver/doc/en/user/services/wms/vendor.md
67. https://github.com/vasturiano/globe.gl/issues/30, https://github.com/vasturiano/globe.gl/commit/77b047b, https://github.com/vasturiano/globe.gl/commit/b35847d
68. https://github.com/Leaflet/Leaflet/issues/5922, https://github.com/Leaflet/Leaflet/pull/4396, ref-repos/Leaflet__Leaflet/CHANGELOG.md
69. https://github.com/Leaflet/Leaflet/commit/13f649d4
70. https://github.com/NASAWorldWind/WorldWindJava/issues/195, https://github.com/NASAWorldWind/WorldWindJava/pull/262, https://github.com/NASAWorldWind/WorldWindJava/pull/274
71. https://github.com/Leaflet/Leaflet/commit/6aaad717
72. https://github.com/TerriaJS/terriajs/commit/39e23d50309f3bf9b26056129d4dc24b8854f0df, https://github.com/TerriaJS/terriajs/commit/fb294d713d08d6263098cddd136f698f55aa710d
73. ref-repos/Leaflet__Leaflet/CHANGELOG.md, https://github.com/Leaflet/Leaflet/pull/3032, https://github.com/Leaflet/Leaflet/pull/2311
74. https://github.com/openlayers/openlayers/issues/4232#issuecomment-203025742, https://github.com/openlayers/openlayers/issues/4232#issuecomment-232299628
75. https://github.com/visgl/deck.gl/pull/4365
76. https://github.com/maplibre/navara/pull/133
77. https://github.com/reearth/reearth-visualizer/pull/920, https://github.com/reearth/reearth-visualizer/commit/3c5a29e7, https://github.com/reearth/reearth-visualizer/commit/25359fa5
78. https://github.com/TerriaJS/terriajs/issues/1211
79. https://github.com/qgis/QGIS/commit/70aea49fc4
80. https://github.com/tangrams/tangram-es/pull/1076
81. https://github.com/maptalks/maptalks.js/commit/e3e51e3e6, https://github.com/maptalks/maptalks.js/commit/c8f78dc6b
82. https://github.com/iTowns/itowns/commit/5d2f384
83. https://github.com/kenjiyoshidahome2026-bit/ortho-earth/blob/main/packages/ortho-core/gint%20draw%20spec.md (local: ref-repos/kenjiyoshidahome2026-bit__ortho-earth/packages/ortho-core/gint draw spec.md)
84. https://github.com/visgl/deck.gl/blob/master/dev-docs/RFCs/v5.3/picking-overlapping-objects-rfc.md, ref-repos/visgl__deck.gl/docs/upgrade-guide.md
85. https://github.com/visgl/deck.gl/pull/1730
86. https://github.com/CesiumGS/cesium/commit/94e579ea4201816bc52c94be45d81cfe0251d60c, https://github.com/CesiumGS/cesium/commit/e3b388d662995577eb4ca7927a413724c2d60538, https://github.com/CesiumGS/cesium/commit/fe8738002a4ee5330124962c927f839ea4c28801
87. https://github.com/openlayers/openlayers/pull/3015
88. skills/map-from-scratch/references/pitfalls.md