Geo3D sorts documents by their distance from a lat/lon box with newOutsideBoxSort. For a box that touches a pole and spans under 180 degrees of longitude, Lucene builds a GeoNorthRectangle (or GeoSouthRectangle): a wedge whose two side edges meet at the pole. Its distance code checks the bottom edge, the sides and the bottom corners, but not the pole.
- You index documents with
Geo3DDocValuesField(WGS84) at (80, 180), just across the North Pole, and at (35, 0), and sort them withnewOutsideBoxSort("loc", 60, 90, -10, 10, WGS84): the box latitude 60..90 x longitude -10..10, which contains the pole. - You expect (80, 180) first: the pole is inside the box and only 10 degrees (about 1,112 km) away, while (35, 0) is 25 degrees (about 2,780 km) south of the box.
- You get (35, 0) first: (80, 180) gets 4,434,716 m, 39.88 degrees, the distance to a bottom corner of the box, (60, 10) or (60, -10). No error; the hits are the same, only the sort values and the order are wrong.
- Why: for a box that reaches one pole and spans under 180 degrees of longitude (and is not a line or a point), Lucene builds a
GeoNorthRectangle, whoseoutsideDistancetakes the minimum over the bottom edge, the two side edges and the two bottom corners, never the pole where both side edges end. From (80, 180) neither side edge offers a point on the box, so a corner wins. - Move the top to 89.9999 and the same point gets 10.0001 degrees (a plain rectangle, correct); at 89.999999 it is 39.88 again. Boxes that reach the South Pole (
GeoSouthRectangle) have the mirror bug; boxes 180 degrees wide or more are fine.
Applications that sort with newOutsideBoxSort (or call computeOutsideDistance on a GeoBBoxFactory box) where the box reaches latitude 90 or -90 and spans under 180 degrees of longitude. Documents beyond the pole (26-37% of outside grid points in the boxes measured) get values too large, mostly by tens of degrees, and sort late or leave the top N.
Mechanism, terms and numbers929 words
Geo3DDocValuesField.newOutsideBoxSort orders documents "by ascending outside distance from the box". For a box whose top latitude is the North Pole (or whose bottom latitude is the South Pole) and whose longitude extent is under 180 degrees, GeoBBoxFactory builds a GeoNorthRectangle (GeoSouthRectangle): a wedge whose two side edges meet at the pole, which is inside the box. Its outsideDistance takes the minimum over the bottom parallel, the two side meridians and the two bottom corners, never the pole. For a point on the far side of the pole neither side meridian offers a point on the box, so the result is the distance to a corner: for the box latitude 60..90 x longitude -10..10 and the point (80, 180), 39.88 degrees instead of 10, and in the WGS84 sort 4,434,716 m, so the document sorts after one at (35, 0), 25 degrees south of the box (sort value 2,779,879 m). In the three boxes measured, 26% to 37% of the outside grid points get values that are too large, most by tens of degrees; the hit set never changes. The same box with a top latitude of 89.9999 is a plain rectangle and gives 10.0001 degrees. The bug dates from the first outside-distance code (LUCENE-6578, 5.3.0, 2015) and is in every release since (the sort since 6.1.0); adding the pole distance to the minimum, as GeoLongitudeSlice already does, fixes it, with all of lucene/spatial3d passing.
- Outside distance: the arc distance from a point to the nearest point of a shape, 0 inside it.
Geo3DDocValuesField.newOutsideBoxSortsorts by it and reports it in meters (the arc distance times the planet's mean radius). - Polar rectangle:
GeoBBoxFactory.makeGeoBBoxbuilds aGeoNorthRectanglewhen the top latitude is the pole, the bottom latitude is not the other pole, the box has nonzero width and height, and its longitude extent is under 180 degrees (strictly, under pi minus pi x 1e-12 rad), aGeoSouthRectanglefor the South Pole (GeoBBoxFactory.java:58-131). Its boundary is the bottom (top) parallel between two corners and two meridian segments from the corners to the pole. - Pole tolerance: the factory treats a latitude as the pole when it is within pi x 1e-12 rad of it or its sine is within 1e-12 of 1 (
GeoBBoxFactory.java:137-150). The sine test reaches about 0.00008 degrees (9 m) from the pole, so 89.999999 and 89.99992 build aGeoNorthRectangleand 89.9999 a plainGeoRectangle. The sine tolerance dates from LUCENE-10673 (first released in 9.4.0); in older releases only latitudes essentially at the pole count.
newOutsideBoxSort(Geo3DDocValuesField.java:281-291) callsGeo3DUtil.fromBox(Geo3DUtil.java:173-189), which checks the latitudes against +/-90 and callsGeoBBoxFactory.makeGeoBBox; the factory returnsGeoNorthRectangle/GeoSouthRectanglefor a pole-reaching box narrower thanGeoWideRectangle.MIN_WIDE_EXTENT(GeoBBoxFactory.java:101,:128-131;GeoWideRectangle.java:31).- The sort's comparator calls
computeOutsideDistance(DistanceStyle.ARC, x, y, z)on each decoded doc value (Geo3DPointOutsideDistanceComparator.java:101,:143), which returns 0.0 inside the box andoutsideDistanceotherwise (GeoBaseMembershipShape.java:47-53). GeoNorthRectangle.outsideDistance(GeoNorthRectangle.java:283-298) returnsmin(bottom, left, right, LRHC, LLHC);GeoSouthRectangle.outsideDistance(GeoSouthRectangle.java:254-269) is the mirror. The pole is never a candidate.- Each side term measures only to the perpendicular foot on the meridian plane and its antipode and drops those outside the box (
Plane.java:600-636). For (80, 180) and the side at longitude -10 the foot is (80.1489, 170), on the far half of the meridian, and the antipode (-80.1489, -10), below the box: the term is +infinity, and the same for the other side. - What is left: the bottom parallel's point (60, 0) at 40 degrees and the corners (60, +-10) at 39.882280 degrees. The pole, 10 degrees away, is never asked.
- The class itself lists the pole as the end point of both side edges (
leftPlanePoints,rightPlanePoints,edgePoints,GeoNorthRectangle.java:156-159) and adds it to its bounds (:279).GeoLongitudeSlice.outsideDistanceadds both poles (GeoLongitudeSlice.java:196-207); the wide polar rectangles search the whole side meridians, so the pole lies on their arc (GeoWideNorthRectangle.java:258-274).
| box | point | Lucene | correct |
|---|---|---|---|
| lat 60..90 x lon -10..10, SPHERE and WGS84 | (80, 180) | 39.882280 degrees (a corner) | 10 degrees (the pole) |
lat 60..89.9999 x lon -10..10 (control: GeoRectangle) | (80, 180) | 10.000098 degrees | 10.000098 degrees |
| lat 60..89.999999 x lon -10..10 (inside the pole tolerance) | (80, 180) | 39.882280 degrees | 10 degrees |
| lat 60..90 x lon -10..10 | (89, 180) | 30.985249 degrees | 1 degree |
| lat 60..90 x lon -100..100 (wide, control) | (80, 180) | 9.846552 degrees | 9.846552 degrees |
lat -90..-60 x lon -10..10 (GeoSouthRectangle) | (-80, 180) | 39.882280 degrees | 10 degrees |
| lat 45..90 x lon -20..30 (SPHERE) | (75, 170) | 57.1244 degrees | 15 degrees |
| lat -90..-40 x lon 100..160 (SPHERE) | (-70, -60) | 66.2143 degrees | 20 degrees |
| WGS84 sort, lat 60..90 x lon -10..10 | (80, 180) vs (35, 0) | 4,434,715.59 m, sorted after 2,779,878.83 m | about 1,111,954 m, sorted first |
In grid sweeps against an independent spherical-trigonometry reference, 226 of 801 outside points (north box lat 45..90 x lon -20..30) and 205 of 790 (south box lat -90..-40 x lon 100..160) were wrong; the worst points were (81.5, -177.5) with 52.65 degrees instead of 8.50 and (-86.5, -45.5) with 52.91 instead of 3.50. For the box lat 60..90 x lon -10..10 a 1-degree grid in the fact check found 37% of the outside points wrong, by up to 30 degrees. The points need not be polar: for the tall box lat -89.5..90 x lon -10..10 the finder's sweep has wrong values near the equator, about 1.2 degrees too large.
Exposure and likelihood464 words
- Who. Users of the optional
lucene/spatial3dmodule who sort withGeo3DDocValuesField.newOutsideBoxSort, or callcomputeOutsideDistanceon aGeoBBoxFactorybox (alone or inside a composite shape), where the factory builds aGeoNorthRectangleorGeoSouthRectangle: a box of nonzero size that reaches latitude 90 or -90 (or lies within the factory's pole tolerance, such as 89.999999), not both, and spans under 180 degrees of longitude, for example a polar sector such as lat 60..90 x lon -10..10, and the index holds documents on the far side of the pole. - Effect. Those documents get sort values that are too large (meters in the
FieldDoc), mostly by tens of degrees, so they sort after farther documents or fall out of the top N. Every returned hit still matches the query. No exception, with assertions on or off. - Default vs opt-in. An optional module and an apparently niche sort: no in-tree caller or test uses it and no matching public report was found (there is no usage data). The caller must pass a
PlanetModel; the repro passesWGS84, withnew IndexWriterConfig()and the codec the test framework picks (Asserting(Lucene104)in the logged run). The bug is in the shape geometry and does not depend on the codec or the writer settings. - Scale. None: one document triggers it, deterministically, at any index size. The share depends on the box: in the independent reproducer's grid sweeps 28% (north box) and 26% (south box) of the outside points were wrong, by up to about 44 and 49 degrees; for the headline box lat 60..90 x lon -10..10 a 1-degree grid in the fact check found 37% wrong, by up to 30 degrees. On the fact check's grid, 81-87% of the wrong values in these three boxes were off by 10 degrees or more.
- Not affected. Boxes whose top (bottom) latitude is outside the factory's pole tolerance, about 0.00008 degrees from the pole (89.9999 is already correct; 89.99992 and 89.999999 are not), boxes spanning 180 degrees or more, boxes away from the poles, and the hit set of any query.
- Workaround. Until a fix ships, a top latitude just outside the tolerance, such as 89.9999, gives correct values; it leaves out a cap of about 11 m around the pole, so points in that cap get a small positive distance instead of 0.
- How likely. Rare: it needs both a pole-reaching narrow box and documents beyond the pole. No public report was found in the 11 years since 5.3.0.
- Related findings (separate reports, other fix sites): LFV-737 (
GeoExactCircleoutside distance skips the slice end points) is the same missing-end-point pattern; LFV-735 (GeoStandardPathmeasures its end caps to the end point instead of the cap edge) is a related outside-distance error at the ends of a shape.
The test shows that the Geo3D outside distance from the point (80, 180) to the box lat 60..90 x lon -10..10 (a GeoNorthRectangle, which contains the north pole) ignores the pole.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial3d.geom;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestPolarRectangleOutsideDistance extends LuceneTestCase {
public void testPointBeyondNorthPole() {
// box lat 60..90 x lon -10..10 contains the north pole, so (80, 180) is 10 degrees from it
GeoBBox box =
GeoBBoxFactory.makeGeoBBox(
PlanetModel.SPHERE,
Math.toRadians(90),
Math.toRadians(60),
Math.toRadians(-10),
Math.toRadians(10));
GeoPoint point = new GeoPoint(PlanetModel.SPHERE, Math.toRadians(80), Math.toRadians(180));
double distance = box.computeOutsideDistance(DistanceStyle.ARC, point);
assertEquals("outside distance in degrees", 10.0, Math.toDegrees(distance), 1e-9);
}
}
# in a copy of this finding's repro folder (repro/LFV-782/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.spatial3d.geom.TestPolarRectangleOutsideDistance
- Expected vs actual
- Expected: 10 degrees (the distance to the pole, which is in the box); actual (-ea and -da): 39.882280145867114 degrees (a far corner), which is the value
Geo3DDocValuesField.newOutsideBoxSortsorts by.
More files, environment, full output37 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: outside distance in degrees expected:<10.0> but was:<39.882280145867114>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:555)
org.apache.lucene.spatial3d.geom.TestPolarRectangleOutsideDistance.testPointBeyondNorthPole(TestPolarRectangleOutsideDistance.java:34)newOutsideBoxSortpromises "ascending outside distance from the box" for any latitude within +/-90; the pole is inside the box.- The class lists the pole as an end point of both side edges and measures the other end points, the corners;
GeoLongitudeSlice, from the same commit, includes the poles. - Not an approximation: top 89.9999 gives 10.0001 degrees, 90 gives 39.88.
The full argument253 words
- The contract.
newOutsideBoxSort: "This sort orders documents by ascending outside distance from the box. Points within the box have distance 0.0." Latitude bounds "must be within standard +/-90 coordinate bounds" (Geo3DDocValuesField.java:260-280).GeoOutsideDistance: shapes "that can compute the distance from a point to the closest outside edge" (GeoOutsideDistance.java:19-21). - The stated intent. LUCENE-6578 (now #7636), which introduced outside distances, asked for the arc distance "from a GeoPoint to the nearest edge/point of a GeoShape".
- The class's own geometry. The pole is inside the box (the repro asserts
isWithin(NORTH_POLE)) and is the end point of both side edges (GeoNorthRectangle.java:156-159); the method already measures the other end points, the corners. - The same commit did it right elsewhere.
688c67f8ff2gaveGeoLongitudeSlice.outsideDistanceitsNORTH_POLE/SOUTH_POLEterms in the same diff. - Not an approximation. On the same point the value jumps from 10.000098 degrees (top 89.9999) to 39.882280 (top 90): about 3,300 km for an 11 m change of the box. spatial3d's documented accuracy caveats are orders of magnitude smaller: the doc-value precision loss (4.19e-8 for the latitude component and 8.38e-8 for longitude,
Geo3DDocValuesField.java:45-47), "up to 0.5% error" for the WGS84 distance query (Geo3DPoint.java:112-113) and the accuracy setting of exact circles (GeoCircleFactory.java:50-57). - Tests and history. No upstream test calls
newOutsideBoxSort; the only box outside-distance assertions use a plain rectangle (lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoBBox.java:75-77). The two methods are unchanged since 2015 except for a reformat (LUCENE-9570), and no CHANGES entry or issue documents a polar limitation.
The devil's-advocate check228 words
The strongest case for "intended" (the intended-behaviour checker built it; cross-model reviews rated it weak): GeoNorthRectangle and GeoSouthRectangle are @lucene.internal, and GeoOutsideDistance, GeoBBox and GeoBBoxFactory are @lucene.experimental, as are PlanetModel, which the sort requires, and Geo3DPoint; the Geo3DDocValuesField class javadoc still lists its factory methods as "TBD" and warns about precision loss; "the closest outside edge" does not say that edge end points count; LUCENE-7212 (#8267), which added the sort, is still open and its last maintainer comment lists tests as still needed; and a fix changes existing sort values and top-N results.
Why it fails: newOutsideBoxSort itself carries no stability tag and promises the ascending outside distance for any latitude in +/-90; "edge" must include its end points, because the method already measures the corners; neither an open feature issue nor a precision warning in the 1e-8 range documents that values may be off by tens of degrees; and the jump between 89.9999 and 90 shows a missing case, not a design. Upstream has treated geo3d distance errors as bugs before (for example LUCENE-7139, "Fix bugs in geo3d's Vincenty surface distance"). The compatibility effect is real; it bears on how the fix is announced (a CHANGES entry), not on whether the current values are correct. Expect a maintainer to say the sort is rarely used: that bears on priority, not on validity.
Recommended Add the pole distance to the minimum
Add the pole to the minimum in GeoNorthRectangle.outsideDistance and GeoSouthRectangle.outsideDistance, as GeoLongitudeSlice does: two added lines and one changed line per class. The repro passes with assertions on and off, and all of lucene/spatial3d passes (185 tests including the repro).
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java
@@ -291,9 +291,11 @@ class GeoNorthRectangle extends GeoBaseBBox {
final double LRHCDistance = distanceStyle.computeDistance(LRHC, x, y, z);
final double LLHCDistance = distanceStyle.computeDistance(LLHC, x, y, z);
+ // the left and right edges end at the pole, which is part of the rectangle
+ final double northDistance = distanceStyle.computeDistance(planetModel.NORTH_POLE, x, y, z);
return Math.min(
- bottomDistance,
+ Math.min(bottomDistance, northDistance),
Math.min(Math.min(leftDistance, rightDistance), Math.min(LRHCDistance, LLHCDistance)));
}
(GeoSouthRectangle: the same with SOUTH_POLE)All options and trade-offs3 options ยท 696 words
Option A was applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF); B and C were not run. Until a fix ships, a top latitude just outside the pole tolerance, such as 89.9999, avoids the bug; it leaves out a cap of about 11 m around the pole.
In GeoNorthRectangle.outsideDistance (:283-298) add distanceStyle.computeDistance(planetModel.NORTH_POLE, x, y, z) to the minimum; in GeoSouthRectangle.outsideDistance (:254-269) the same with SOUTH_POLE. The boundary is the bottom (top) parallel and two meridian segments ending at the corners and the pole; the existing terms cover the parallel, the segments' interiors and the corners, so the pole is the only missing end point. For a point whose perpendicular feet fall off both segments, the nearest point of each segment is an end point, so this completes the minimum.
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java
@@ -291,9 +291,11 @@ class GeoNorthRectangle extends GeoBaseBBox {
final double LRHCDistance = distanceStyle.computeDistance(LRHC, x, y, z);
final double LLHCDistance = distanceStyle.computeDistance(LLHC, x, y, z);
+ // the left and right edges end at the pole, which is part of the rectangle
+ final double northDistance = distanceStyle.computeDistance(planetModel.NORTH_POLE, x, y, z);
return Math.min(
- bottomDistance,
+ Math.min(bottomDistance, northDistance),
Math.min(Math.min(leftDistance, rightDistance), Math.min(LRHCDistance, LLHCDistance)));
}
(GeoSouthRectangle: the same with SOUTH_POLE)Trade-offs
- Cost: one extra point distance per call, only in the two narrow polar classes. No format, API or index change.
- Compatibility: existing users of the sort get smaller values for documents beyond the pole, a different order and possibly a different top N; worth stating in the CHANGES entry.
- Distance styles: the pole is a point of the box for every
DistanceStyle, and the term uses the samecomputeDistance(GeoPoint, x, y, z)as the corners; onlyARC, the style the sort uses, was validated. - With the PR: add the repro (or a polar case in
TestGeoBBoxplus a firstnewOutsideBoxSorttest inTestGeo3DDocValues) and a CHANGES entry.
What the checks validated
- Stock main
a9779ef3c193(gradle, seed DEADBEEF): the repro fails 3 of 4 with assertions on and off;TestGeoBBox(11 tests) andTestGeo3DDocValues(1) pass. - With the fix: the repro passes 4 of 4 with assertions on and off;
TestGeoBBoxandTestGeo3DDocValuespass; all of:lucene:spatial3d:testwith the repro class added passes (20 classes, 185 tests, 0 failures, 3 skipped);ecjLintMain,ecjLintTestandcheckGoogleJavaFormatpass for spatial3d;fix.diffapplies cleanly to a clean tree. - With the fix shadowed onto the main jars: the repro 4 of 4, the hunt's JUnit 3 of 3, the independent reproducer's JUnit 7 of 7 and the cross-model reviewer's own test 5 of 5 pass.
- In the hunt: the upstream spatial3d suite gives the same result with and without the change (181 tests: 178 pass, 3 assumption-aborted, on
ea57360912ed), and the bounded sweep drops from 81,742 to 0 violations in 17,068,396 checks (not re-run independently).
Say in the newOutsideBoxSort javadoc that boxes reaching a pole give too large values beyond the pole, and keep the current values. Users can work around it with a top latitude such as 89.9999.
Trade-offs
Keeps existing sort values and top-N results stable and is cheaper to review, but leaves errors of tens of degrees (up to about 49 in the boxes measured) for a legal input when a two-line fix exists, and the factory's pole tolerance (89.999999 counts as 90) makes the trigger hard to describe.
GeoWideNorthRectangle bounds its side planes only by the latitude plane, which is safe because, for an extent of 180 degrees or more, the far half of each side meridian lies inside the box (GeoWideNorthRectangle.java:262-267).
Trade-offs
For a narrow box the far half of each side meridian is outside the box, so the same change would make distances too small: for (80, 180) the left-side candidate (80.1489, 170), 1.727941 degrees away, is rejected only by the right side plane, so dropping that bound would return 1.73 degrees instead of 10 (the diagram probe trace; the change itself was not run).
Downstream relevance: an application that takes the first hit of this sort as the document nearest to a polar box can get a farther one; hit sets and filters are not affected.
A bounded-exhaustive comparison of the real jar against an exact reference: 540 lat/lon boxes and about 600 polygons, each checked at about 3,700 to 5,400 points on the sphere and on WGS84, 17.07 million checks. All 81,742 mismatches were in GeoNorthRectangle and GeoSouthRectangle, always too large; the cause was then read off the code and reduced to a JUnit.
The method in full234 words
Method: bounded-exhaustive translation validation on the stock jar (semi-formal tier), in a hunt unit on the geo3d distance sort fields.
- The harness built 540
GeoBBoxFactoryboxes (9 latitudes including +-90 and +-89.5 x 12 longitude pairs including dateline-crossing, full, wide and degenerate boxes) and 600 simple star polygons (599 built on WGS84), and compared each one's outside distance at about 3,700 to 5,400 decoded doc-value points with an independent exact reference of the arc distance to the boundary. Geo3D parallels are horizontal planes and its meridians planes through the origin, so the same direction-based reference is exact on SPHERE and WGS84 for Lucene's ARC distance (the angle between the points' direction vectors, not an ellipsoidal surface distance). Only ARC was checked, and 27,398 checks within 3e-6 rad of an edge were skipped. - Of 17,068,396 checks, 81,742 failed, all in two classes:
GeoNorthRectangle(20,963 per planet model) andGeoSouthRectangle(19,908 per planet model), always with Lucene's value too large. With the pole term added, 0. - The unit's plan targeted the sort comparators; it did not name this lead. The cause was read off the two
outsideDistancemethods and reduced to a public-API JUnit on the stock main jars. - Found 2026-10-27 on main
a9779ef3c193; then checked by an independent reproducer, an intended-behaviour devil's advocate, reviews by a model from another family (OpenAI GPT-5.6), an adjudication, a history, prior-report and security follow-up, and a credibility audit.
No prior report found (2026-10-27) in two runs of the hunt's prior-report tool (63 and 65 queries), manual GitHub, JIRA, list and web searches, and the changed files of all 409 open PRs. The closest items introduced outside distances and the sort without discussing poles.
Searches4 searches
Verdict new: these searches found no report of this bug by anyone. All 26 logged hits were judged not this bug. Keyword search cannot prove absence: list results were judged from subjects and snippets, web results are capped at about 10-25 per query, and private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; Elasticsearch, OpenSearch, Solr; dev@, java-user@, issues@, commits@; the JIRA archive; the web | 2026-10-27 | two runs of the hunt's prior-report tool (63 and 65 queries), plus the finder's 51 and the devil's advocate's 45 queries | 26 logged hits, none the same bug; context only: #7636 (LUCENE-6578, introduced outside distances, never mentions poles), #8267 (LUCENE-7212, open, added the sort fields) |
| apache/lucene, apache/lucene-solr, apache/solr and global GitHub | 2026-10-27 | 13 manual issue/PR searches, one code search, the commits API | GeoNorthRectangle membership, bounds and construction fixes #8256, #8283, #8345, #9503, #9014, #11544, all leaving outsideDistance unchanged |
| JIRA LUCENE and SOLR; mailing lists; the web | 2026-10-27 | 12 JIRA queries, 11 list queries, 16 web searches | SOLR-8636 (Won't Fix, another code path); nothing on point |
| all 409 open apache/lucene PRs | 2026-10-27 | changed-file scan | none touches the five files, except that one, #16911 (a branch_10x deletion PR), was too large to list fully; #14775 (adds inverseSort()) is the only open PR on the outside-sort code and leaves the geometry alone |
There from the start: GeoNorthRectangle and GeoSouthRectangle.outsideDistance were first written without a pole term in LUCENE-6578 (688c67f8ff2, 2015-07-01, released in 5.3.0); the sort came in 6.1.0 (LUCENE-7212). Executed on 14 releases from 5.3.0 to 10.5.2 and main; not fixed upstream.
Versions tested7 rows
Introduced by LUCENE-6578 (now #7636, "Geo3d: arcDistanceToShape() method may be useful"): trunk 688c67f8ff2 (svn r1688545, 2015-07-01, "LUCENE-6578: Geo3D: compute the distance from a point to a shape.", committed by @dsmiley; CHANGES credits Karl Wright and David Smiley; JIRA patches, no PR) and the branch_5x backport 780e4040238, first released in 5.3.0. The same diff gave GeoLongitudeSlice.outsideDistance its pole terms.
Later changes to the two methods: only the LUCENE-9570 reformat (2cbf261032d, 2021); the files moved with LUCENE-6607 and LUCENE-7056. The sort newOutsideBoxSort came with LUCENE-7212 (cc263413457, branch_6x aa339a2f075, 2016-05-18, #8267, still open) and had the bug from its first release, 6.1.0. Release-level bracketing only; no commit-level build.
The near-pole trigger is younger: latitudes such as 89.999999 count as the pole only through the sine tolerance of LUCENE-10673 (bd0718f0716, branch_9x 17e8a42e1aa, 2022-08-04, first released in 9.4.0); before that, the pole test was within pi x 1e-12 rad (in 9.2-9.3 also a sine of exactly 1), so older releases are affected only at the exact pole latitudes.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the repro fails 3 of 4 through gradle (assertions on and off) and the classpath runner (-ea, -da); passes with the fix |
| 10.4.0, 10.5.2 | affected | the four-test repro fails 3 of 4 (10.5.2 with -ea and -da, 10.4.0 with -da), the same values as on main |
| 6.1.0, 6.6.6, 7.7.3, 8.5.0, 8.11.4, 9.0.0, 9.12.3, 10.5.2 | affected | the sort test fails 3 of 3: the far-side document sorts last (4,434,715.59 m against 2,779,878.83 m) |
| 5.3.0, 5.3.2, 5.5.5, 6.0.0, 6.0.1 | shape API | the shape test fails 3 of 3 (39.882280145867114 degrees instead of 10, north and south); no sort API yet |
| 5.3.0 with the pole term added | control | passes 3 of 3, so the 5.3.0 failure is this defect |
| 5.2.1 | no API | no outside-distance API (the shape test does not compile) |
688c67f8ff2 / 780e4040238 (LUCENE-6578) | introducing | in all 11 release tags 5.3.0 to 5.5.5 (backport) and the 83 release tags 6.0.0 to 10.5.2 in the history clone, which has no tags for 8.8.2 and 8.9.0-8.11.4 (8.11.4 was executed and fails) |
An independent reproducer and an intended-behaviour devil's advocate reproduced it with their own tests and controls; a cross-model reviewer (OpenAI GPT-5.6) reproduced it with its own test, and a second one rated the intended-behaviour case weak; an adjudication and an audit re-ran it with new seeds, and this doc's gradle runs reproduce it on a9779ef3c193.
Checks and verdicts453 words
Every executing check wrote or re-ran its own repro on stock main; every control passed and every fix check passed. Not independently re-run: the finder's bounded-sweep counts and the commit-level introduction (release bracketing only).
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Bounded-exhaustive sweep (81,742 violations, only these two classes; 0 with the fix), then a 3-test public-API JUnit: 3 of 3 fail on main (-ea, -da) and on 6.6.6 through 10.5.2; pass with the fix. |
| cross-model review, refute lens (OpenAI GPT-5.6) | real, high | Its own test (different boxes and points, a south mirror and a wide-box control) fails 3 of 5 with -ea and -da; passes 5 of 5 with the fix; TestGeoBBox and TestGeo3DDocValues still pass with it; intended-behaviour argument weak. |
| independent reproducer | real | Own JUnit with a spherical-trigonometry reference: 5 of 7 fail on main (-ea, -da), 10.5.2 and 9.12.3, both controls pass; 7 of 7 with the fix. |
| cross-model review, intended lens (the cross-model review, OpenAI GPT-5.6, reading only) | real, medium | Root cause agreed, inputs in contract, intended-behaviour argument weak; a separate run re-ran the finder's JUnit (3 of 3 fail). |
| intended-behaviour devil's advocate | real | Built the strongest case for intended behaviour and rejected it; its probe shows the jump from 10.000098 degrees (top 89.9999) to 39.882280 (90 and 89.999999), and the wide box correct. |
| adjudication | confirmed, high, priority Low | Re-ran the independent reproduction's JUnit with a new seed (5 of 7 fail with -ea and -da, identical values) and with the fix (7 of 7 pass). |
| Registration check and credibility audit | keep | Re-ran the finder's JUnit (3 of 3 fail, -ea and -da, new seeds) and the independent reproduction's (5 of 7), the fix (3 of 3 pass), re-read the cited lines and checked the expected value by hand. |
| fact checks of this doc (an independent checker and a cross-model reviewer, OpenAI GPT-5.6) | bug, mechanism and numbers confirmed; wording errors fixed | The independent checker re-ran the gradle repro on a clean extraction of a9779ef3c193 (3 of 4 fail with assertions on and off; 4 of 4 with the fix), the independent reproducer's JUnit (5 of 7) and the repro with a new seed on main, ea57360912ed and 10.5.2, and recomputed every number with its own probe; the cross-model reviewer re-checked the claims against the code, logs and history (read-only). Their corrections (scale figures, the pole tolerance, older-release compatibility of the repro, wording) are applied. |
| this doc | reproduced; fix validated | Gradle in a clean worktree at a9779ef3c193: the repro fails 3 of 4 (control passes) with assertions on and off; with the fix 4 of 4 pass and all of lucene/spatial3d passes (185 tests). |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 504 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,434 words
- Registry title
- Geo3D outside distance to a lat/lon box that reaches a pole (GeoNorthRectangle / GeoSouthRectangle) ignores the pole, so Geo3DDocValuesField.newOutsideBoxSort gives documents beyond the pole the distance to a far corner (39.88 instead of 10 degrees for the box lat 60..90 x lon -10..10 and the point (80, 180)) and sorts them after nearer documents
- Area
- spatial3d / geom polar rectangles outside distance (GeoNorthRectangle, GeoSouthRectangle) and the Geo3DDocValuesField outside-box sort
- Priority
- Low, rank 411: Geo3D outside-distance sorts are wrong only for pole-reaching boxes narrower than 180 degrees in the little-used spatial3d sort, so it follows the similar outside-distance rows LFV-274 and LFV-735.
- Impact class
- silent wrong results
- Impact note
- Applications that sort by Geo3DDocValuesField.newOutsideBoxSort (or call computeOutsideDistance on a GeoBBoxFactory box) with a box whose maxLatitude is 90 or minLatitude -90 (also latitudes within the factory's pole tolerance, such as 89.999999) and whose longitude extent is under 180 degrees get distances tens of degrees too large for documents on the far side of the pole, so those documents sort after farther ones or drop out of the top N, with no error, on the default planet model, codec and config and with -ea and -da alike; the hit set never changes, and boxes that stop short of the pole (89.9999 already gives the right value) or span 180 degrees or more of longitude are not affected.
- Summary
- You sort documents by their outside distance from a lat/lon box that reaches the North Pole, for example latitude 60..90 x longitude -10..10, with Geo3DDocValuesField.newOutsideBoxSort. A document at (80, 180) is only 10 degrees (about 1,112 km) from that box, because the box contains the pole, so it should sort before a document at (35, 0), which is 25 degrees south of the box. Instead GeoNorthRectangle.outsideDistance never considers the pole, where both side edges of the box end, and returns the distance to a far corner (39.88 degrees on the sphere; 4,434,716 m against 2,779,879 m in the WGS84 sort), so the nearer document sorts last; GeoSouthRectangle has the mirror bug at the South Pole.
- Introduced
- 688c67f8ff22b72b9df4ed2f92d26b909563b2b1 (trunk, svn r1688545, 2015-07-01, 'LUCENE-6578: Geo3D: compute the distance from a point to a shape.', committed by GitHub dsmiley; issue LUCENE-6578, now apache/lucene#7636; SVN/JIRA era, no PR), released through the branch_5x backport 780e4040238713c2ff85c4b981cbc6b80aa4094f (svn r1688546, in 5.3.0) [read: git log -L on outsideDistance in both classes in the history clone: the methods were first written in this commit without a pole term, while the same diff gave GeoLongitudeSlice.outsideDistance its NORTH_POLE / SOUTH_POLE terms; the only later change is the 2cbf261032d reformat (LUCENE-9570)] [exec: release bracketing, 5.2.1 has no outside-distance API and 5.3.0 fails; no commit-level build]. Not a regression. The user-facing sort came later with the bug from its first release: Geo3DDocValuesField.newOutsideBoxSort, LUCENE-7212 (trunk cc263413457, branch_6x aa339a2f075, 2016-05-18, apache/lucene#8267) (2015-07-01 (trunk 688c67f8ff2 and branch_5x backport 780e4040238); the outside-box sort API on 2016-05-18 (LUCENE-7212))
- First affected version
- 5.3.0 [exec] for the geom API (GeoBBoxFactory.makeGeoBBox(...).computeOutsideDistance); 5.2.1, the release before, has no outside-distance API [exec: API probe passes, shape test does not compile]. 6.1.0 [exec] for Geo3DDocValuesField.newOutsideBoxSort (6.0.1 has no Geo3DDocValuesField [exec: compile error])
- Affected versions
- Every release from 5.3.0 to 10.5.2 and main a9779ef3c193; not fixed upstream. [exec] the shape test (SPHERE 39.882280145867114 degrees instead of 10, north and south) fails 3 of 3 on 5.3.0, 5.3.2, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 9.12.3, 10.5.2 and main; the sort test fails 3 of 3 on 6.1.0, 6.6.6, 7.7.3, 8.5.0, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main (-ea and -da); with the pole term shadowed in, 5.3.0 and main pass; the independent reproduction's own JUnit fails 5 of 7 (its 2 controls pass) on main (-ea, -da), 10.5.2 and 9.12.3. [read] git tag --contains: 780e4040238 is in all 11 releases 5.3.0-5.5.5 and 688c67f8ff2 in the 83 release tags 6.0.0-10.5.2; GeoNorthRectangle.java and GeoSouthRectangle.java at a9779ef3c193 are byte-identical to the study base 7cecf795236, and lucene/spatial3d is unchanged between ea57360912ed and a9779ef3c193 (git diff --stat empty). All runs on JDK 25.0.3
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (searched 2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (63 and 65 queries over GitHub issues, PRs and commits of apache/lucene, downstream Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web), the finder's 51-query and the intended-behaviour check's 45-query runs, manual GitHub searches (13 issue/PR searches in apache/lucene, lucene-solr, solr and globally, one code search, the commits API), JIRA LUCENE and SOLR (12 queries), 11 mailing-list queries, 16 web searches, the changed files of all 409 open apache/lucene PRs and the full git history. Context, not reports: LUCENE-6578 / #7636 (closed, 5.3; introduced outside distances with the stated intent 'distance from a GeoPoint to the nearest edge/point of a GeoShape'; no discussion of poles), LUCENE-7212 / #8267 (open; added the outside-distance sort fields; its last maintainer comment lists tests as still needed), GeoNorthRectangle test failures LUCENE-7201 / #8256, LUCENE-7228 / #8283, LUCENE-7290 / #8345 and LUCENE-8457 / #9503 and the pole fixes LUCENE-7965 / #9014 and LUCENE-10508 / #11544 (membership, bounds and construction; outsideDistance unchanged), SOLR-8636 (Won't Fix; another code path). The only open PR touching the outside-sort code, #14775, adds inverseSort() and leaves the geometry alone. Not fixed: outsideDistance in both classes is unchanged in substance since 688c67f8ff2 on main (last file change dfd536186fe, 2026-04-01) and branch_10x (de3b294be44, 2023-10-11); CHANGES.txt has no entry. Gaps: keyword searches only, list results judged from subjects and snippets, web results capped at about 10-25 per query, private reports not visible
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): the repro JUnit through gradle in a clean worktree, stock and with the fix, assertions on and off, plusTestGeoBBox,TestGeo3DDocValuesand all oflucene/spatial3d(20 classes, 185 tests) with the fix (2026-10-28); through the hunt's classpath runner, the same JUnit on main (-ea, -da, fix shadowed), on 10.5.2 (-ea, -da) and on 10.4.0 (-da). Hunt and history runs: the shape test on 5.3.0, 5.3.2, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 9.12.3, 10.5.2 and main, the sort test on 6.1.0, 6.6.6, 7.7.3, 8.5.0, 8.11.4, 9.0.0, 9.12.3, 10.5.2 and main; some hunt reviews on mainea57360912ed(identical spatial3d code). OpenJDK 25.0.3, Linux aarch64.
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java:22-28 (at a9779ef3c193) (class javadoc 'including the north pole', @lucene.internal), :156-159 (leftPlanePoints / rightPlanePoints = {NORTH_POLE, corner}, edgePoints = {NORTH_POLE}), :279 (getBounds addPoint(NORTH_POLE)), :283-298 (outsideDistance = min(bottom, left, right, LRHC, LLHC), no pole term)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoSouthRectangle.java:148-151 (at a9779ef3c193) (side-edge points and edgePoints contain SOUTH_POLE), :250 (getBounds addPoint(SOUTH_POLE)), :252-269 (outsideDistance = min(top, left, right, ULHC, URHC), no pole term)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoLongitudeSlice.java:196-207 (at a9779ef3c193) and geom/GeoDegenerateLongitudeSlice.java:157-158 (sibling outsideDistance methods include the NORTH_POLE and SOUTH_POLE distances)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBBoxFactory.java:72, (at a9779ef3c193) :112, :129, :131 (latitude zone for full longitude, wide rectangle for extents of PI or more, GeoNorthRectangle / GeoSouthRectangle for a pole-reaching box narrower than PI)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:260-290 (at a9779ef3c193) (newOutsideBoxSort: 'This sort orders documents by ascending outside distance from the box. Points within the box have distance 0.0.'; latitude bounds 'within standard +/-90'; no stability tag) and Geo3DUtil.java fromBox (GeoUtils.checkLatitude / checkLongitude, then GeoBBoxFactory.makeGeoBBox)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoOutsideDistance.java:17-23 (at a9779ef3c193) ('compute the distance from a point to the closest outside edge', @lucene.experimental) and :25-55; Geo3DPointOutsideDistanceComparator.java:101, :143 (the sort value is computeOutsideDistance(DistanceStyle.ARC, ...) of the decoded point)lucene/spatial3d/src/test: (at a9779ef3c193) no test calls newOutsideBoxSort (grep); TestGeoBBox.java:75-77 asserts outside distance only for a plain rectangle
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:260-291; Geo3DUtil.java:173-189; Geo3DPointOutsideDistanceComparator.java:101, 125, 143lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBBoxFactory.java:58, 83, 94, 101, 119, 128-131, 137-150; geom/Vector.java:26, 29; geom/GeoWideRectangle.java:31; geom/GeoBaseMembershipShape.java:47-53lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoNorthRectangle.java:22-28, 156-159, 279, 283-298; geom/GeoSouthRectangle.java:148-151, 250, 254-269lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Plane.java:600-636; geom/GeoLongitudeSlice.java:196-207; geom/GeoDegenerateLongitudeSlice.java:152-161; geom/GeoWideNorthRectangle.java:258-274
- Who is affected
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCompositeMembershipShape.java:55-69 (a composite's outside distance is the minimum over its members); geom/GeoCompositeMembershipShape.java:27, geom/GeoCompositeAreaShape.java:28 (public)lucene/spatial3d/src/test (grep: no call of newOutsideBoxSort)
- Why it looks unintended
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:40 (TBD), 45-47 (precision warning), 260-280; geom/GeoOutsideDistance.java:19-21; geom/GeoNorthRectangle.java:22-28 (@lucene.internal), 156-159; geom/PlanetModel.java:26 and Geo3DPoint.java:42 (@lucene.experimental); Geo3DPoint.java:112-113; geom/GeoCircleFactory.java:50-57lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoBBox.java:75-77
- Suggested fix
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoLongitudeSlice.java:196-207; geom/GeoWideNorthRectangle.java:258-274
- TestPolarRectangleOutsideDistance.java (JUnit test):
repro/LFV-782/src/org/apache/lucene/spatial3d/geom/TestPolarRectangleOutsideDistance.java(36 lines)
Made in parallel with this doc; its values come from a tracing probe on main a9779ef3c193 (assertions on and off, and with the fix), and its code lines from a9779ef3c193.
assets/LFV-782/bug.png(504 KB)
