Skip to content

perf(mayIntersect): check a large query box once instead of tile by tile - #979

Open
cldellow wants to merge 1 commit into
systemed:masterfrom
cldellow:perf/mayintersect-bound
Open

cldellow wants to merge 1 commit into
systemed:masterfrom
cldellow:perf/mayintersect-bound

Conversation

@cldellow

@cldellow cldellow commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Gain: Intersects() / FindIntersecting() calls that took minutes now take milliseconds, when the feature's bounding box falls inside a huge shapefile/GeoJSON polygon's bounding box but doesn't touch the polygon.

The stock OMT profile doesn't call Intersects or FindIntersecting, so numbers in this PR refer to a custom profile of mine that ran into the issue.

Problem

Before running the exact intersects(), tilemaker checks a z15 bitmap. A tile's bit is set if it falls inside the bounding box of any indexed polygon. The first time a query hits a set bit, tilemaker intersects that tile with the polygon to confirm it and caches the answer. It goes tile by tile until one confirms.

When a feature lies inside a polygon's bounding box but outside the polygon, no tile ever confirms. So tilemaker intersects every z15 tile under the feature with the polygon. For a marine reserve a couple of degrees across, that's tens of thousands of intersections, each against the whole polygon.

Two marine reserves off British Columbia (13 nodes each), master, 1 thread:

FindIntersecting('states') FindIntersecting('countries')
r20818206 10.3 s 28.0 s
r20972138 43.4 s 431.5 s

Fix

If more than 16 of the feature's z15 tiles would need confirming, intersect the feature's bounding box with the candidate polygons once instead. If none intersect it, none can intersect a tile inside it, so the answer is false. Otherwise the exact check decides, as before. Features covering 16 tiles or fewer keep the per-tile check and its cache.

This is only a pre-filter before the exact intersects() in intersectsQuery(), so results can't change.

Results (British Columbia, 16 threads)

profile before after
search index: FindIntersecting on states, countries and parks for 17,600 features 2m03s 9.9s
roads: Intersects() on parks for every way 8.86 s 8.92 s
POIs: same 5.59 s 5.31 s
buildings: same 6.92 s 7.13 s

For the search-index profile, every feature got the same full, sorted list of matches with and without this PR.

The two marine reserves above now take about 0 s per lookup.

A new test/shp_mem_tiles.test.cpp (in make test) covers small and large boxes, both touching the polygon and inside its empty bounding box, before and after caching.


The investigation, patch, test and benchmarks were done with Claude Code. I reviewed the results.

🤖 Generated with Claude Code

mayIntersect() resolves the z15 tiles of a query box that the rough
bounding-box bitmap flags, one exact intersects() with the candidate
geometries per tile, until one intersects. A large query box that lies
within a huge geometry's bounding box without touching it, such as an
offshore marine reserve inside a country's bounding box, checks every one
of thousands of tiles against that geometry and finds nothing.

When more than 16 tiles would need that check, intersect the query box
with the candidates once instead: if nothing intersects it, nothing
intersects any tile in it. Otherwise return true and leave it to the
caller's exact check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01736kJW6d4B5wvq7B5vaKD9
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.

1 participant