Skip to content

fix(spatial): cover the antimeridian and the poles in Near queries (#166) - #174

Merged
mrdevrobot merged 3 commits into
EntglDb:mainfrom
CaffeinatedCoder:fix/166-near-antimeridian-poles
Oct 13, 2026
Merged

mrdevrobot merged 3 commits into
EntglDb:mainfrom
CaffeinatedCoder:fix/166-near-antimeridian-poles

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Collaborator

Fixes #166.

Stacked on #173 (fix for #165). Until that merges, this PR also shows its commit. Only the last commit, fix(spatial): cover the antimeridian and the poles in Near queries, belongs here. It relies on the exact distance check that #173 adds in RTreeIndex.SearchNear.

Problem

The R-Tree query box for Near was lat ± dLat, lon ± dLon, compared against raw coordinates:

  • Across ±180°: a box around longitude −179.9 extends to −180.x and never reaches +179.9.
  • Over a pole: a circle that contains the pole reaches every longitude, but the box still only spans lon ± dLon.
  • At high latitude: the longitude half-width radius / cos(lat) is measured at the center latitude, but the circle is widest further toward the pole. Around (80°N, 0°) with a 556 km radius the box spans ±28.8° while the circle reaches ±30.1°, so points near its widest part were missed even with no pole or meridian involved.

Fix

SpatialMath.NearQueryBoxes replaces BoundingBox. The old method's only caller was RTreeIndex.SearchNear; the two unused internal overloads are removed.

  • A circle that contains a pole (lat ± dLat reaches ±90°) gets one box over all longitudes, from its far edge up to the pole. One that contains both poles covers the whole globe.
  • Otherwise the longitude half-width is the circle's widest point, asin(sin r / cos lat) with r the angular radius. The argument stays below 1 whenever no pole is inside.
  • A box that crosses ±180° is split into one box on each side. The two halves cannot overlap because the half-width stays below 90°, so no point is returned twice.

RTreeIndex.SearchNear searches each box and keeps the exact great-circle check from #173, which trims the extra area. Both LINQ Near and typed and dynamic NearAsync go through it.

Tests

NearAntimeridianPoleTests adds 5 tests. All fail without this commit. Each checks NearAsync and LINQ Near and requires them to agree:

  • The issue's antimeridian repro, queried from both sides.
  • The issue's over-the-pole repro, plus a point that is out of range.
  • A circle around the south pole finds points at five longitudes, including −180 and 179.
  • A point at the circle's widest longitude at 80°N.
  • DynamicCollection.NearAsync across the antimeridian.

Full suite on macOS: 2395 passed, 3 failed, 4 skipped. The 3 failures are the known MultiProcessWalSharedMemoryTests .wal-shm lock timeouts.

Not in scope

The issue's open question about Within with min.Longitude > max.Longitude is unchanged. It still matches nothing, in LINQ (index and in-memory) and in $geoWithin alike. Whether it should mean a box wrapping across ±180° or be rejected as invalid is your call. Either is a small follow-up on top of NearQueryBoxes.

🤖 Generated with Claude Code

app-reviewer

This comment was marked as outdated.

@mrdevrobot mrdevrobot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed only 3450e15. The math checks out: asin(sin r / cos φ) is the spherical-cap longitude half-width and the non-pole branch guarantees |φ| + r < 90°; the pole branch covers r ≥ 90°, lat = ±90, r = 0 and r ≥ πR; the split boxes can't overlap since dLon ≤ 90°, so no point is returned twice; GeoBox argument order matches. 523/523 targeted tests pass, full suite green apart from the known .wal-shm failures, no conflicts with main (5.2.3). Approving once #173 lands; two robustness notes.

  1. SpatialMath.cs:49: the pole guard is done in degrees but the asin argument is sin(r)/cos(lat); when |lat| + r is within a few ulp of 90° the ratio can round to 1 + ε, Asin returns NaN, both longitude bounds become NaN and GeoBox.Intersects then matches every entry (all comparisons false). The distance check keeps the result correct, but it becomes a full index walk. Clamp with Math.Min(1.0, ...) or do the pole test on the same ratio (sin r >= cos lat).
  2. An unnormalised centre longitude (lon = 190) produces one inverted (empty) box and one over-wide box; still correct after the distance check. Normalising lon into [-180, 180) at the top keeps the boxes tight.

Nits: RTreeIndex.cs:66 "the boxes never overlap" deserves the one-line reason (dLon ≤ 90°); the whole-globe case could short-circuit before computing anything when angularRadius >= Math.PI.

@CaffeinatedCoder

Copy link
Copy Markdown
Collaborator Author

Thanks. Both notes are addressed in the latest commit, on top of the updated #173 (merged in; the IndexOptimizer.cs line-ending churn is gone from this branch too).

1. NaN near the pole. The pole test is now done on the same ratio that feeds asin: sin r >= cos lat, alongside the degree checks. The argument can therefore no longer exceed 1. New test NearQueryBoxes_AtThePoleBoundary_NeverContainNaN sweeps centres from 60° to 90° with radii at and just under the exact pole distance and asserts that no bound is NaN. To be transparent: that sweep passes on the previous commit too, because it doesn't hit the exact ulp case. It is a guard, not a reproduction.

2. Unnormalised centre. The centre longitude is normalised into [-180, 180) first. New test Near_WithAnUnnormalisedCentreLongitude_MatchesTheNormalisedOne checks that NearQueryBoxes(0, 190, 50) equals NearQueryBoxes(0, -170, 50) and that the query results match. It fails on the previous commit.

Nits:

  • The comment in SearchNear now gives the reason: a split happens only when no pole is inside the circle, so dLon < 90°.
  • angularRadius >= Math.PI short-circuits to the whole globe, with a test.

Full suite on macOS: 2414 passed, 0 failed, 4 skipped.

🤖 Generated with Claude Code

mrdevrobot
mrdevrobot previously approved these changes Oct 12, 2026

@mrdevrobot mrdevrobot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All points are fixed. I scanned about 1.06M (lat, radius) pairs within ±40 ulp of the pole distance: the old code produced 109 NaN boxes, the new one none. A 2M-sample random scan with lon in [-360, 360] and r up to 20,000 km found no NaN, inverted or out-of-range boxes. Normalisation maps ±180 and ±540 to -180 with the right split. Full suite on this branch 2414 / 0 / 4 skipped.

Approving, to land after #173.

One test request: NearQueryBoxes_AtThePoleBoundary_NeverContainNaN passes with the fix reverted, because the 0.37° sweep never hits the rounding case. Please add an explicit input that did produce NaN, for example lat = -88.3413, radius = 184.4390248253298 km. NearQueryBoxes_LargerThanHalfTheEarth_CoverTheWholeGlobe also passes when reverted, since the degree check already returns the whole globe. That one is harmless.

@CaffeinatedCoder

Copy link
Copy Markdown
Collaborator Author

Thanks. Added in 256e8e5: NearQueryBoxes_AtThePoleBoundary_KnownRoundingCase_HasNoNaN, with lat = ±2.1702 and radius = 9766.228168206266 km (the distance to the pole). Both cases give NaN boxes with the pre-fix SpatialMath.cs and pass with the fix.

I didn't use lat = -88.3413, radius = 184.4390248253298. With the pre-fix code on this branch, lat - dLat evaluates to exactly -90.0 there, so the old minLat <= -90 check already returns the whole-globe box. The ratio sin(r)/cos(lat) is 1.0000000000000002, but asin is never reached, and the test passed with the fix reverted. An input only slips through when lat ± dLat rounds just inside ±90° while the ratio rounds above 1. Searching 4-decimal latitudes found only a handful, all near the equator. If you saw NaN with that input on another base, let me know and I'll add it as well.

Agreed on LargerThanHalfTheEarth_CoverTheWholeGlobe; I left it as is.

@mrdevrobot mrdevrobot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified 256e8e5, and you're right about the input I suggested: with the pre-fix code, lat ± dLat lands exactly on ±90° there, so the old degree check already returned the whole globe and asin was never reached. Your ±2.1702 / 9,766.228 km case does produce NaN boxes before the fix and passes after it. On main with #169/#170 merged the full suite passed (2434 / 0 / 4 skipped).

#173 is now merged as a squash (efacaf4), and this branch still carries its earlier commits (224b332, 97bfb7d), so it conflicts with main. Please rebase onto main keeping only 3450e15, 8ba4628 and 256e8e5, and mark it ready for review. I'll approve and merge once CI is green.

CaffeinatedCoder and others added 3 commits October 13, 2026 15:11
…ntglDb#166)

The R-Tree query box for Near was plain lat ± dLat, lon ± dLon. A box
around longitude -179.9 never reached +179.9, and a circle that contains a
pole reaches every longitude while its box only spanned lon ± dLon. The
longitude half-width radius / cos(lat) was also too narrow at high
latitudes: the circle is widest poleward of its center.

SpatialMath.NearQueryBoxes replaces BoundingBox:
- a circle that contains a pole gets one box over all longitudes, from its
  far edge up to the pole;
- otherwise the half-width is asin(sin r / cos lat), and a box that crosses
  ±180° is split into one box on each side.

RTreeIndex.SearchNear searches each box and keeps the exact great-circle
distance check, so the extra area is trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Db#166)

- The pole case is decided on the same ratio that feeds asin
  (sin r >= cos lat), so rounding can no longer produce an asin argument
  above 1 and NaN bounds that match every index entry.
- The centre longitude is normalised into [-180, 180), so an unnormalised
  centre (e.g. 190) yields the same tight boxes as its normalised form.
- A radius of at least half the Earth's circumference short-circuits to a
  whole-globe box.
- The non-overlap comment in SearchNear states why (dLon < 90°).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lDb#166)

The 0.37 degree sweep in NearQueryBoxes_AtThePoleBoundary_NeverContainNaN
never hits the rounding case, so it passed with the fix reverted. The new
cases (lat +/-2.1702, radius 9766.228168206266 km, the distance to the
pole) give NaN boxes without the sin(r) >= cos(lat) check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder force-pushed the fix/166-near-antimeridian-poles branch from 256e8e5 to c82db82 Compare October 13, 2026 13:13
@CaffeinatedCoder
CaffeinatedCoder marked this pull request as ready for review October 13, 2026 13:13
@CaffeinatedCoder

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (efacaf4), keeping only the three #166 commits: 3450e15 → a510ec2, 8ba4628 → b64355a, 256e8e5 → c82db82. The cherry-picks applied without conflicts. The PR now touches only RTreeIndex.cs, SpatialMath.cs and NearAntimeridianPoleTests.cs. Full suite on the rebased branch: 2467 / 0 / 4 skipped. Marked ready for review.

@mrdevrobot mrdevrobot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the rebase: the three commits (a510ec2, b64355a, c82db82) are identical to 3450e15, 8ba4628 and 256e8e5 that I reviewed earlier, and CI is green on the rebased head. Merging.

@mrdevrobot
mrdevrobot merged commit a61880e into EntglDb:main Oct 13, 2026
2 checks passed

This branch was successfully deployed

1 active deployment
Production — c82db828 Deployed Oct 13, 2026 by CaffeinatedCoder via build_and_publish #669
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.

Near misses points across the ±180° meridian and over the poles

3 participants