A Geo3D box whose top and bottom latitude are equal and which spans all longitudes is the line of that latitude; Lucene builds it as GeoDegenerateLatitudeZone. Its plane, bounds and intersection test use the planet model at geo3d's 1e-12 resolution, but its point test compares z with sin(latitude), the unit-sphere value, at 1e-10.
- You build the line of latitude 0.5 rad on WGS84:
GeoBBoxFactory.makeGeoBBox(WGS84, 0.5, 0.5, -PI, PI), top and bottom latitude equal, all longitudes. You expect it to contain the point (0.5, 1.0) and its owngetCenter(). - You get false for both, while the same line over longitudes -3..3 contains the point. The full-width line instead accepts a point 0.0108 degrees (about 1.2 km) south of it.
- Why: its
isWithinisMath.abs(z - sin(lat)) < 1e-10.sin(lat)is the line's z only on a unit sphere (on WGS84 the z at 0.5 rad is 0.479591, not 0.479426), and 1e-10 is 100 times the 1e-12 that the zone's own intersection test uses. - The loose tolerance hurts on every planet model. On the default sphere, a rectangle 1.4 rad tall whose top lies 5e-11 rad (0.3 mm) below latitude 0.4 gets
getRelationship= CONTAINS: its one sampled corner passes the 1e-10 test, and no edge crossing is found at 1e-12. - In search: with the Geo3D spatial context and a prefix tree, an Intersects query for
rect(-180, 180, 45.000000001, 45.000000001)matches all six indexed points at latitudes 10 to 40; the same line over -179..179 matches none. No error, with assertions on or off.
Users of the optional spatial3d module, or of spatial-extras with the Geo3D context, who build a zero-height box over all longitudes. On WGS84 its point test rejects the line's own latitude and accepts another parallel. In a Geo3D prefix-tree field on the default sphere, such a line matches points tens of degrees away when placed just above a grid edge.
Mechanism, terms and numbers1,031 words
GeoBBoxFactory.makeGeoBBox(planetModel, lat, lat, -PI, PI), a box whose top and bottom latitude are equal and which spans all longitudes, is the line of latitude lat; the factory builds a GeoDegenerateLatitudeZone for it. Its constructor builds the right plane for that line on the chosen planet model, and intersects, getBounds and outsideDistance use it (the center point is built on the planet model, on the line), but isWithin returns Math.abs(z - this.sinLatitude) < 1e-10. That is wrong twice: sin(lat) is the line's z only on the unit sphere, so on WGS84 the zone rejects the points on its own latitude, even its own getCenter() (at every latitude except the equator and within less than 1e-7 rad of ±35.2 degrees), and accepts another parallel (0.0108 degrees, about 1.2 km, away at 0.5 rad); and 1e-10 is 100 times geo3d's 1e-12 point-on-plane resolution (Plane.evaluateIsZero), which this zone's own intersection test and the sibling line shapes use, so on the sphere, the Geo3D spatial context's default, a rectangle whose top lies a sampled 1.5e-12 to 1.05e-10 rad below the line gets getRelationship = CONTAINS. On an ellipsoid that 1e-10 window sits around the wrong parallel instead, so there rectangles whose top lies at that parallel get CONTAINS.
Through the spatial-extras Geo3D spatial context that becomes WITHIN for whole prefix-tree cells: an Intersects query for rect(-180, 180, 45.000000001, 45.000000001) matched every indexed point between latitudes 10 and 40, while the same line over [-179, 179] matched none. The same line over a longitude range narrower by more than pi x 1e-12 rad at an end is a different class that tests the same plane with plane.evaluateIsZero and is correct. The line dates from the first geo3d release, 5.2.0 (2015); the fix is one line, return plane.evaluateIsZero(x, y, z);.
- Zero-height, full-width box:
GeoBBoxFactory.makeGeoBBox(pm, L, L, -PI, PI), with top and bottom latitude equal (within pi x 1e-12 rad, or with sines within 1e-12, which near the poles lets in heights up to about 5.9e-7 rad) and longitudes -180..180 (each end within pi x 1e-12 rad). Geometrically it is the parallel of latitude L. The factory builds aGeoDegenerateLatitudeZonefor it; the same line over a range narrower by more than that is aGeoWideDegenerateHorizontalLineorGeoDegenerateHorizontalLine. - Planet model and plane: geo3d puts points on an ellipsoid such as WGS84 or on the unit sphere (SPHERE, the default of the spatial-extras Geo3D spatial context). A parallel is the horizontal plane z = sin(L) x magnitude(L). That equals sin(L) only on the sphere, at the equator (both are 0) and, on WGS84, at about 35.2 degrees, where the magnitude is 1.
- Resolution: geo3d decides "on the plane" with
Vector.MINIMUM_RESOLUTION= 1e-12 (Plane.evaluateIsZero). - Relationship:
GeoArea.getRelationship(shape)answers CONTAINS (the area is inside the shape), WITHIN, OVERLAPS or DISJOINT.GeoBaseAreaShapedecides it from a few sampled edge points plus one intersection test. The spatial-extras prefix tree uses it to decide whether a grid cell is entirely inside the query shape (WITHIN) or only touched.
GeoBBoxFactory.makeGeoBBoxreturnsGeoDegenerateLatitudeZonefor a full-width box with equal latitudes (GeoBBoxFactory.java:58-69, equality tests:145-154; a box narrowed by 3.1e-12 rad at each end is still this class, by 3.2e-12 it is not).Geo3DPoint.newBoxQuery(Geo3DPoint.java:145-155, throughGeo3DUtil.fromBox,:173-189) and spatial4jrectin the Geo3D context (Geo3dShapeFactory.java:164-173) reach it.- The constructor builds
plane = new Plane(planetModel, sinLatitude)(GeoDegenerateLatitudeZone.java:59), the plane z = sinLat x magnitude (Plane.java:88-91). The center/edge point (:61-62),intersects(:110-118),getBounds(:121-124) andoutsideDistance(:144-147) use the planet model or this plane. isWithin(:88-91) isreturn Math.abs(z - this.sinLatitude) < 1e-10;: the unit-sphere z, at a tolerance 100 times the 1e-12 resolution. The literal 1e-10 appears nowhere else in the geom package.- WGS84: at 0.5 rad a point on the latitude has z = 0.47959063 but sin(0.5) = 0.47942554 (difference 1.65e-4), so every point on the line, including the zone's own center, is rejected, and the parallel 0.0108 degrees (1.2 km) south is accepted. At 1.2 rad the difference is -1.68e-3, and the accepted parallel lies about 0.27 degrees north. CLARKE_1866 behaves the same. Only at the equator and within less than 1e-7 rad of ±35.2 degrees, where the ellipsoid's z equals sin(lat), does the zone accept its own latitude.
- Sphere (the Geo3D spatial context's default): for a rectangle just below the line, the rectangle's one sampled edge point (its upper-left corner,
GeoRectangle.java:183) passes the 1e-10 test, the line's center is outside the rectangle, and the 1e-12 intersection test finds no crossing, soGeoBaseAreaShape.getRelationship(GeoBaseAreaShape.java:102-133) returns CONTAINS. A gap probe at latitude 0.4: CONTAINS for the sampled gaps 1.5e-12 to 1.05e-10 rad (a finer sweep: 1.1e-12 to 1.08e-10, that is 1e-12 / cos(0.4) to 1e-10 / cos(0.4)), the correct OVERLAPS when touching (0 to 9e-13), DISJOINT at 2e-10; a rectangle above the line and the narrower line give OVERLAPS when touching and DISJOINT at every separated gap. On WGS84 and CLARKE_1866 those rectangles are DISJOINT (correct): the 1e-10 window sits around the wrong parallel, and rectangles whose top lies at that parallel (0.0147 degrees below the line at 0.4 rad, 0.0108 degrees at 0.5 rad) get CONTAINS instead; only at the equator and at 35.196 degrees, where the two parallels coincide, do the ellipsoids show the sphere's case. - Search: in the Geo3D context a grid cell is a
Geo3dRectangleShape(QuadPrefixTree.java:335,Geo3dShapeFactory.java:164-173), so a cell is related to the query line throughGeo3dShape.relate(Geo3dShape)(Geo3dShape.java:55-58, 77-79): the cell'sgetRelationship(line)of step 5, whichGeo3dShape.relatemaps from CONTAINS to WITHIN (Geo3dShape.java:70-71). All documents of a WITHIN cell are collected without a geometry check (IntersectsPrefixTreeQuery.java:70-75;CompositeSpatialStrategy'sIntersectsRPTVerifyQuery.java:226-249), and at index time a WITHIN cell is stored as fully covered (FilterCellIterator.java:48-51). For the line at 45.000000001 degrees (1.75e-11 rad above 45), the 90 x 45 degree cells[-180, -90] x [0, 45]and[0, 90] x [0, 45]relate WITHIN it; for the[-179, 179]line they are DISJOINT.
Exposure and likelihood536 words
- Who. Users of the optional
lucene/spatial3dmodule who build a box with equal top and bottom latitude (or within the equality limit: about 3e-12 rad at most latitudes, up to about 5.9e-7 rad near the poles) over all 360 degrees of longitude, throughGeoBBoxFactory,Geo3DPoint.newBoxQuery/newShapeQuery, or a spatial4jrectin the spatial-extras Geo3D spatial context (prefix-tree strategies, includingCompositeSpatialStrategy). For example a "points on this parallel" query, or such a line indexed as a shape. - Geometry API. On the ellipsoids (WGS84, CLARKE_1866) such a box rejects the points of the latitude it was built for, at every latitude except the equator and within less than 1e-7 rad of ±35.2 degrees, and accepts another parallel instead: 0.0108 degrees away at 0.5 rad, at most about 0.015 degrees at the sampled latitudes up to 40 degrees, 0.14 at 60, 0.27 at 1.2 rad, 0.73 at 80 and 2.85 at 86 degrees; from about 87 degrees up it accepts no point at all. Points away from both parallels are still rejected. On the sphere the points of the line are accepted, and so are points up to 1e-10 off its plane. Deterministic; no exception, with assertions on or off.
- Search. In a Geo3D prefix-tree field on the default sphere model, a zero-height, full-width query line whose latitude lies just above a coarse grid edge (by about 1e-12 / cos(lat) to 1e-10 / cos(lat) rad) matches whole cells below it: in the repro every point from 10 to 40 degrees. The prefix tree's grid approximation for this query is 2.25 degrees, far below these 5 to 35 degree errors. An indexed line of the same kind is stored as covering those cells, so it matches searches across the band below it (one such document matched 324 of 1,296 small probe searches). On WGS84 a round 45.000000001 has no effect; the trigger is a computed latitude whose accepted parallel lies just above a grid edge (22.5148379785 degrees for the 22.5-degree edge).
Geo3DPointqueries. A point on the line decodes up to about 2.3e-10 off the plane (half of the 4.7e-10 encoding step), so a zero-height box rarely matches the points of its own latitude, with or without this bug. On WGS84 the full-width box matched them at none of 400 sampled latitudes and the narrower box at 4 (the repro picks such a latitude, 28.609289316492603 degrees: 0 of 35 points against 35 for the[-179, 179]box); on the sphere the full-width box matched them at 162 of 400 (its 1e-10 tolerance catches part of the rounding) and the narrower box at none.- Default vs opt-in. Opt-in modules; the Geo3D spatial context defaults to the sphere model.
GeoBBoxFactory,Geo3DPoint,Geo3dShapeandRecursivePrefixTreeStrategyare@lucene.experimental. - Scale. None: one shape, point or document triggers it, at any index size.
- Not affected. Boxes narrower than the full circle by more than pi x 1e-12 rad (other classes, correct), boxes taller than the equality limit, the membership of on-line points on the sphere and at the equator, and core
LatLonPointqueries. - How likely. Rare: the shape is unusual and the search-level effects need specially placed latitudes. No user report was found in the eleven years since 5.2.0.
The test shows that the geo3d box GeoBBoxFactory.makeGeoBBox(WGS84, 0.5, 0.5, -PI, PI) (zero height, all longitudes; a GeoDegenerateLatitudeZone) rejects a point on its own latitude.
/*
* 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 TestGeoZeroHeightFullLongitudeBox extends LuceneTestCase {
public void testContainsPointOnItsLatitude() {
GeoBBox box = GeoBBoxFactory.makeGeoBBox(PlanetModel.WGS84, 0.5, 0.5, -Math.PI, Math.PI);
GeoPoint point = new GeoPoint(PlanetModel.WGS84, 0.5, 1.0);
// expected: true, as for the same box over longitudes [-3, 3]; actual: false
assertTrue("expected " + box + " to contain " + point + ", got false", box.isWithin(point));
}
}
# in a copy of this finding's repro folder (repro/LFV-614/, 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.TestGeoZeroHeightFullLongitudeBox
- Expected vs actual
- Expected
isWithin(new GeoPoint(WGS84, 0.5, 1.0))to be true, as it is for the same box over longitudes [-3, 3]. Actual: false, becauseisWithincompares z with sin(latitude) instead of using the zone's WGS84 plane.
More files, environment, full output41 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: expected GeoDegenerateLatitudeZone: {planetmodel=PlanetModel.WGS84, lat=0.5(28.64788975654116)} to contain [lat=0.5, lon=1.0([X=0.4743231607827104, Y=0.7387145545857223, Z=0.47959063085955106])], got false
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.assertTrue(Assert.java:42)
org.apache.lucene.spatial3d.geom.TestGeoZeroHeightFullLongitudeBox.testContainsPointOnItsLatitude(TestGeoZeroHeightFullLongitudeBox.java:27)- The class contradicts itself: on WGS84 it rejects its own center; its point test disagrees with its own plane and intersection test.
- The narrower line and the sibling degenerate shapes test at 1e-12; the literal 1e-10 appears nowhere else in geo3d.
GeoArea's CONTAINS means the area is entirely within the shape; a line cannot contain a 1.4 rad tall rectangle.
The full argument332 words
- The contracts.
Membership.isWithin: "Check if a point is within this shape" (Membership.java:36-44).GeoBBoxFactory.makeGeoBBox: "@return a GeoBBox corresponding to what was specified." (GeoBBoxFactory.java:35). The class javadoc: "an area rectangle of one specific latitude with no longitude bounds" (GeoDegenerateLatitudeZone.java:24).Geo3DPoint.newBoxQuery: "@return query matching points within this box" (Geo3DPoint.java:142).GeoArea: CONTAINS is "The referenced shape CONTAINS this area" (GeoArea.java:34-35), to be returned when "this GeoArea is entirely within the shape" (GeoArea.java:47-49). - The class contradicts itself. Its center/edge point, "A point on the world that's also on the zone" (
GeoDegenerateLatitudeZone.java:38-39), fails its ownisWithinon WGS84 and CLARKE_1866 at every tested latitude except the equator (it passes only there and within less than 1e-7 rad of ±35.2 degrees); itsisWithindisagrees with its own plane-basedintersectsand bounds. - Siblings.
GeoWideDegenerateHorizontalLine,GeoDegenerateHorizontalLine,GeoDegenerateVerticalLineandGeoDegenerateLongitudeSliceall test their plane withplane.evaluateIsZeroat 1e-12; the degenerate point compares coordinates at the same 1e-12; the literal 1e-10 occurs only here, with no comment. - History points to an oversight. The line was written for the sphere-only first version of geo3d (April 2015, replacing an exact
z == sinLatitude). The WGS84 change five weeks later (LUCENE-6487) moved this class's plane, center point,expandandintersectsto the planet model and leftisWithin. - Precedents. Upstream fixed the analogous precision problem in the sibling
GeoDegeneratePoint.isWithinas a bug (LUCENE-7967, #9016) and fixed degenerate-latitude box failures as bugs (LUCENE-10508 / #11544 in 9.2.0, LUCENE-10673 / #11708 in 9.4.0). - Not an approximation. The search errors are 5 to 35 degrees against the prefix tree's 2.25-degree grid approximation for this query, and the same line over
[-179, 179]matches nothing. - Tests. No upstream test names the class.
TestGeoBBox.testBBoxDegeneratebuilds one on the sphere (TestGeoBBox.java:38-46) but asserts through the polygon'sisWithin, and passes with the fix;TestGeo3DPoint's random box test usesshape.isWithinas its own oracle, so it cannot see this.
The devil's-advocate check230 words
The strongest case for "intended" (built by the intended-behaviour checker; cross-model reviews rated it weak, and the cross-model fact check of this doc rated it moderate, mainly for compatibility): GeoDegenerateLatitudeZone is package-private and @lucene.internal, and the public APIs that reach it are @lucene.experimental; the 1e-10 could be deliberate leniency for a shape with no area, and on the sphere it does let a zero-height Geo3DPoint box match the points of its latitude more often; a zero-height box is arguably ill-posed; prefix trees are approximate by design; the code is eleven years old and unreported; and the fix changes relationships, hits and the cells indexed for such shapes.
Why it fails: the public factory returns this class for any legal full-width box with equal latitudes, and @lucene.experimental permits API change, not wrong answers; no comment, javadoc or commit message explains the 1e-10; the leniency exists only for the full-width class and, apart from the equator and ±35.2 degrees, only on the sphere (the narrower box never had it); the zone's own intersects uses 1e-12, which is what produces the forbidden CONTAINS; the WGS84 rejection is a self-contradiction (the zone rejects its own center), not a tolerance choice; upstream has repeatedly fixed degenerate-box precision errors as bugs; and the false search matches are 5 to 35 degrees away against a 2.25-degree grid approximation, with the [-179, 179] control returning none.
Recommended Test the zone's own plane
Make isWithin test the zone's own plane, as every sibling shape does: return plane.evaluateIsZero(x, y, z);, one line. The repro passes with assertions on and off, and all of lucene/spatial3d (183 tests) and lucene/spatial-extras (118 tests) pass with the repro classes added.
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java
@@ -87,7 +87,7 @@ class GeoDegenerateLatitudeZone extends GeoBaseBBox {
@Override
public boolean isWithin(final double x, final double y, final double z) {
- return Math.abs(z - this.sinLatitude) < 1e-10;
+ return plane.evaluateIsZero(x, y, z);
}All options and trade-offs4 options · 832 words
Option A was applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF); B and C were not run.
Replace the body of GeoDegenerateLatitudeZone.isWithin (:88-91) with plane.evaluateIsZero(x, y, z). The plane's normal is (0, 0, 1), so this is |z + D| < 1e-12 with D from the planet model: the same z test, against the right z, at the resolution that the zone's intersects and every sibling use. It removes both symptoms: on WGS84 the points on the latitude are inside, and at latitude 0.4 on the sphere, WGS84 and CLARKE_1866 the gap probe gives DISJOINT for every gap of 1.5e-12 or more and the correct OVERLAPS for touching rectangles (near a pole a 1.5e-12 rad gap is less than 1e-12 in z, so DISJOINT starts at a wider gap there).
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java
@@ -87,7 +87,7 @@ class GeoDegenerateLatitudeZone extends GeoBaseBBox {
@Override
public boolean isWithin(final double x, final double y, final double z) {
- return Math.abs(z - this.sinLatitude) < 1e-10;
+ return plane.evaluateIsZero(x, y, z);
}Trade-offs
- Tighter tolerance: points 1e-12 to 1e-10 off the plane are no longer accepted, which is what the narrower
[-179, 179]line already does, so the two become consistent. Geo3DPointresults change: a full-width, zero-heightGeo3DPoint.newBoxQuerythen matches exactly what the narrower box matches. On WGS84 it gains the points of its latitude where they decode onto the plane (4 of 400 sampled latitudes, 0 before); on the sphere it loses the points of its latitude that decode 1e-12 to 1e-10 off the plane (matched at 162 of 400 sampled latitudes before, none after, like the narrower box). Matching such points reliably would need a tolerance tied to the encoding step for every box class, a separate change.- Index change for prefix-tree shapes: no file format or API change, but an indexed zero-height, full-width line in a Geo3D prefix-tree field is no longer stored as covering whole cells (in the follow-up probe one such document matched 324 of 1,296 probe searches before the fix and 0 after). Existing indexes keep the old cells until those documents are reindexed; query-side results change as soon as the fixed code runs.
- The protected
sinLatitudefield stays; it is no longer read after construction. - With the PR: add the two tests (or a case in
TestGeoBBoxplus one inTestGeo3dRpt) and a CHANGES entry under Bug Fixes.
What the checks validated
- Stock main
a9779ef3c193(gradle, seed DEADBEEF): the repro fails 3 of 3 with assertions on and off; the closest suites pass:TestGeoBBox(11 tests),TestGeo3DPoint(17, 2 skipped) and the otherorg.apache.lucene.spatial.spatial4j.*classes (21 tests, 4 skipped). - With the fix: the repro passes 3 of 3 with assertions on and off; the same closest suites pass; all of
:lucene:spatial3d:test(20 classes, 183 tests, 0 failures, 3 skipped) and all of:lucene:spatial-extras:test(27 classes, 118 tests, 0 failures, 5 skipped), both with the repro classes added, pass;ecjLintMain,ecjLintTestandcheckGoogleJavaFormatpass for spatial3d,ecjLintTestandcheckGoogleJavaFormatfor spatial-extras;fix.diffapplies cleanly to a clean tree. - With the fix shadowed onto the main jars: the repro 3 of 3, the hunt's JUnits 3 of 3, the independent reproducer's 5 of 5 and the security probe's reach test pass; as a back-port, the version-adapted checks pass on 5.2.0 and 5.3.0.
- In the hunt: the spatial3d suite (181 tests, 0 failures) and the spatial-extras suite (117 tests, the same result as without the fix) on
ea57360912ed.
Compare z with the plane's z (-plane.D) but keep the 1e-10 tolerance.
Trade-offs
Removes the WGS84 rejection but keeps the 1e-10 vs 1e-12 mismatch, so the false CONTAINS and the prefix-tree false matches stay, and move to just below the line on every planet model; it would also keep the sphere's partial Geo3DPoint leniency and extend it to the ellipsoids (from reading the code; not run).
Decide nesting from more than one sampled point per shape.
Trade-offs
That is the general nesting question of the separate tangent-shapes finding (LFV-378, an internal id; public context in #8990 / LUCENE-7941), a larger and riskier change; this case does not need it, because with option A touching rectangles already give OVERLAPS and separated ones DISJOINT.
GeoBBoxFactory could throw for such boxes or document them as unsupported. Workaround until a fix ships: narrow the longitude range by more than pi x 1e-12 rad at an end (for example to [-PI + 1e-9, PI - 1e-9]), which selects GeoWideDegenerateHorizontalLine (it contains (0.5, 1.0) on WGS84 and gives DISJOINT for the sphere rectangle); a narrowing of 3.1e-12 rad still selects the zone.
Trade-offs
Changes the public factory's contract for legal input, while the one-line fix already makes the class consistent with its siblings; the workaround leaves a 2e-9 rad sliver at the antimeridian.
Downstream relevance: an application that lets users index their own shapes into a Geo3D prefix-tree field can get one such line, placed just above a grid edge (on the default sphere model; on WGS84 at a computed latitude), matching searches across the whole latitude band below it (324 of 1,296 probe searches in an executed probe).
A bounded-exhaustive check of the real jar against the geometric definition of a box: 10,117 boxes on each of the sphere and WGS84, membership at about 110 million points, then 33.6 million area/shape relationship pairs. The only robust membership errors were this class on WGS84; the relationship pass then found its false CONTAINS on the sphere.
The method in full190 words
Method: bounded-exhaustive translation validation on the stock jar (semi-formal tier), in a hunt unit on the geo3d bounding-box and solid families.
- One mode built every box of a 10,117-box lattice (zero-height, zero-width, wider than PI, polar and dateline-crossing boxes) on SPHERE and WGS84 through
GeoBBoxFactory.makeGeoBBoxand comparedisWithinwith the geometric box definition at about 1.1e8 lattice and edge points plus 124,762 points constructed on degenerate boxes. Its only robust membership violations wereGeoDegenerateLatitudeZoneon WGS84: 510 on-latitude points rejected. - A second mode, run on separate build machines, checked 33.6 million area x shape relationship pairs and found 360 sphere pairs in which the zone built from latitudes 0.4 + 2e-12 and 0.4 was reported to CONTAIN rectangles and zones whose top latitude is 0.4.
- The cause was then read off the code, confirmed by a one-line fix that removes both, and turned into public-API JUnits on the stock main jars.
- Found 2026-10-26 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, reading only), an adjudication, a history, prior-report and security follow-up, and a credibility audit.
No prior report found (2026-10-26) in 178 queries over GitHub, JIRA, the mailing lists and the web, plus the changed files of all 407 open PRs. The closest items fixed constructor failures for near-equal latitudes and a precision bug in the sibling degenerate point, without touching this class.
Searches4 searches
Verdict new: no report by anyone. All 17 logged hits were judged not this bug, and the file is unchanged on upstream main a95c754ee003, branch_10x and branch_10_6. Keyword search cannot prove absence: the class is package-private, so a report worded only through GeoBBoxFactory, Geo3DPoint or a Geo3D rect query very differently could be missed; one web query was throttled; 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-26 | the hunt's prior-report tool (51 queries: 45 logged here, its 6 web queries counted with the web searches), plus the finder's earlier run (51 queries, 359 distinct hits, 0 high overlap) | nothing on point; context only: #7258 (LUCENE-6196, the sphere-only check-in), #7546 (LUCENE-6487, the WGS84 change; the class appears only in a compile log) |
| GitHub (apache/lucene, Elasticsearch, OpenSearch, Solr, lucene-solr, spatial4j, Lucene.NET) | 2026-10-26 | 74 GraphQL searches, including a sweep of the module:spatial3d label (107 issues, 27 PRs), and 5 REST searches | #11544 / LUCENE-10508 and #11708 / LUCENE-10673 (constructor failures for near-equal latitudes; their fixes tuned GeoBBoxFactory.latitudesEquals without touching this class); #9016 / LUCENE-7967 (precision fix in GeoDegeneratePoint.isWithin); #8955, #9036 (open relationship issues about other shapes) |
| JIRA LUCENE and SOLR; mailing lists; the web | 2026-10-26 | 15 JIRA, 24 list and 8 web searches; 7 direct reads and checks (31 threads read in full) | nothing on point |
| all 407 open apache/lucene PRs | 2026-10-26 | changed-file scan | none touches GeoDegenerateLatitudeZone.java or GeoBBoxFactory.java |
Since the first geo3d release: the 1e-10 test against sin(latitude) was written in April 2015 (LUCENE-6196, released in 5.2.0); the WGS84 change (LUCENE-6487, 5.3.0) moved the rest of the class to the planet model but not this test. It fails on 10 releases from 5.2.0 to 10.5.2 and on main; not fixed upstream.
Versions tested7 rows
The line: 1242bf1519ca (2015-04-22, svn feature branch lucene6196, LUCENE-6196, now #7258; "committing Karl's latest patch", a patch by Karl Wright committed by @dsmiley; svn era, no PR) wrote Math.abs(z - this.sinLatitude) < 1e-10, replacing the exact z == sinLatitude of the class's creation (400c39bac77, same unreleased branch). Merged to trunk f760a51f80d and branch_5x 536ae683ff6 on 2015-05-06; first release 5.2.0, which has only the unit sphere, so there the defect shows as the false CONTAINS and the prefix-tree false matches.
The WGS84 half: 560f3a1a0104 (2015-05-27, branch lucene6487, LUCENE-6487, now #7546; trunk 77de0ed1cf8, branch_5x 8c3e6b0ed32, 2015-06-02) moved the plane, the center point, expand and intersects to the planet model and left isWithin; first release with the WGS84 rejection: 5.3.0. All 96 release tags 5.2.0 to 10.5.2 in the history clone contain the line; upstream main a95c754ee003, branch_10x and branch_10_6 too.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the repro fails 3 of 3 through gradle (assertions on and off) and the classpath runner (-ea, -da); passes with the fix |
| 10.5.2, 9.12.3, 8.11.4 | affected | geometry checks fail; the hunt's JUnits fail 3 of 3 (10.5.2 also with this doc's repro, 3 of 3) |
| 7.7.3 | affected | geometry checks and the hunt's prefix-tree JUnit fail |
| 5.3.0, 5.5.5, 6.0.0, 7.0.1 | main() checks | WGS84 on-latitude point rejected, sphere CONTAINS, 4 prefix-tree false hits (control 0) |
| 5.2.0, 5.2.1 | sphere only | sphere CONTAINS and 4 prefix-tree false hits (control 0); no planet models yet, so no WGS84 part |
| 5.2.0, 5.3.0 with the fix | control | every check passes, so the oldest failures come from this line |
| 5.1.0 | no API | no geo3d package (compile error) |
An independent reproducer and an intended-behaviour devil's advocate reproduced it with their own tests; cross-model reviews (OpenAI GPT-5.6, reading only) agreed on the root cause or rated the intended-behaviour case weak; an adjudication, the registration check and an audit re-ran it with new seeds; this doc's gradle runs reproduce it and validate the fix.
Checks and verdicts487 words
Every executing check wrote or re-ran its own repro on stock main; every control passed and every fix check passed. The cross-model reviews could not run code; separate runs re-ran the hunt's JUnits.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive sweep, then 3 public-API JUnits: 3 of 3 fail on main (-ea, -da), 10.5.2, 9.12.3 and 8.11.4, the prefix-tree test on 7.7.3; pass with the fix; spatial3d (181 tests) green with the fix, spatial-extras (117) unchanged. |
| independent reproducer | real, high | Own 5 JUnits fail 5 of 5 on main (-ea, -da), 10.5.2 and 9.12.3; pass with the fix; its gap probe (sphere, latitude 0.4) gives the false CONTAINS at the sampled gaps 1.5e-12 to 1.05e-10 rad. |
| cross-model review, root-cause lens (the cross-model review, OpenAI GPT-5.6, reading only) | real, medium | Root cause agreed: two defects at one site (the missing ellipsoid magnitude and the 1e-10 vs 1e-12 tolerance). The driving AI tool's runs: -da 3 of 3 fail, fix 3 of 3 pass, a numeric probe confirms both mechanisms. |
| cross-model review, intended lens (OpenAI GPT-5.6, reading only) | real, medium | Intended-behaviour argument weak. |
| cross-model review, refute lens (OpenAI GPT-5.6, reading only) | not real, low (procedural) | Could not run code and voted not real on procedure only; it agreed with the root cause and called the intended argument weak; a separate re-run of the spatial3d JUnit failed 2 of 2. |
| intended-behaviour devil's advocate | real | Built the strongest case for intended behaviour and rejected it; re-ran the hunt's JUnits (3 of 3 fail on main -ea/-da and 10.5.2); self-check: the zone rejects its own center on WGS84 and CLARKE_1866; a 90 x 45 degree cell relates WITHIN the line. |
| adjudication | confirmed, high | Re-ran the independent reproduction's JUnits (5 of 5 fail with -ea and -da) and the hunt's with -da (3 of 3 fail); the independent reproduction's pass 5 of 5 with the fix. |
| Registration check and credibility audit | keep | Re-ran the hunt's JUnits (3 of 3 fail, -ea and -da, new seeds) and the independent reproduction's (5 of 5), and the fix (3 of 3 pass); re-read the cited lines. |
| this doc | reproduced; fix validated | Gradle in a clean worktree at a9779ef3c193: the repro fails 3 of 3 (every control passes) with assertions on and off; with the fix 3 of 3 pass and all of lucene/spatial3d (183 tests) and lucene/spatial-extras (118 tests) pass. |
| fact checks of this doc | bug holds; text corrected | An independent fact check re-ran the repro on main (-ea, -da), 10.5.2 (-ea, -da) and 9.12.3 (-da), 3 of 3 failing each time, and with the fix (3 of 3 pass), and probed every model; a cross-model fact check (OpenAI GPT-5.6, reading only) reviewed the text. Their corrections (sphere-only scope of the false CONTAINS, the relate path, the box count, the offsets, the fix's compatibility effects) are applied here. |
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 524 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,455 words
- Registry title
- GeoDegenerateLatitudeZone.isWithin compares z with sin(latitude) at a 1e-10 tolerance instead of using its own planet-model plane, so a zero-height geo3d box spanning all longitudes rejects the points on its own latitude on WGS84 and, on any planet model, can report CONTAINS for areas just below it, which makes Geo3D prefix-tree searches match points tens of degrees away
- Area
- spatial3d / geom bounding boxes (GeoDegenerateLatitudeZone, which GeoBBoxFactory.makeGeoBBox returns for a box with equal top and bottom latitude spanning all longitudes), reached through Geo3DPoint.newBoxQuery / newShapeQuery and the spatial-extras Geo3D spatial context (Geo3dShape.relate, RecursivePrefixTreeStrategy, CompositeSpatialStrategy)
- Priority
- Low, rank 414: A zero-height, all-longitude geo3d box gives wrong membership on WGS84 and false prefix-tree matches inside tiny 1e-12 rad bands, an unusual shape that is rarer than LFV-611, so it follows it.
- Impact class
- silent wrong results
- Impact note
- Hits applications that use the opt-in spatial3d module, or spatial-extras with the Geo3D spatial context, with a zero-height box or rectangle that spans all 360 degrees of longitude (top latitude equal to bottom, or within about 3e-12 rad so that GeoBBoxFactory treats them as equal). In the geometry API the error is deterministic on every non-spherical planet model such as WGS84: isWithin rejects every surface point on the box's latitude, even the box's own getCenter(), and accepts a parallel 0.01 to 0.3 degrees away. Query-level effects need specially placed latitudes: a Geo3DPoint.newBoxQuery miss needs a latitude whose plane passes within 1e-12 of a decoded z (the narrower [-179, 179] box has the same limit), and the prefix-tree false matches (query side: points 3 to 40 degrees away; document side: one indexed line matching searches up to 42.5 degrees away) need the latitude 1e-12 to about 1e-10 rad above a coarse grid-cell edge. Nothing throws and no scale is needed.
- Summary
- You build a geo3d box whose top and bottom latitude are equal and which spans all longitudes, for example GeoBBoxFactory.makeGeoBBox(WGS84, 0.5, 0.5, -PI, PI), and expect it to contain exactly the points on that latitude, as the same line over [-179, 179] degrees does. Its isWithin compares a point's z with sin(latitude), the unit-sphere value, with a 1e-10 tolerance, instead of the planet-model plane that the class builds and uses everywhere else, so on WGS84 it rejects every point on its own latitude (and accepts a parallel about 1.2 km away). On any planet model, including the default sphere of the Geo3D spatial context, the 100x looser tolerance makes getRelationship report that the line CONTAINS a rectangle just below it, so a prefix-tree Intersects query for rect(-180, 180, 45.000000001, 45.000000001) matched 4 points at latitudes 10, 10, 30 and 44 degrees, while the [-179, 179] control matched none.
- Introduced
- 1242bf1519ca (LUCENE-6196 'Include geo3d package, along with Lucene integration to make it useful', issue #7258; commit message 'committing Karl's latest patch ... r/33353 (diff #9)', a patch by DaddyWri committed by dsmiley on the svn branch lucene6196 r1675374; reached trunk through f760a51f80d and branch_5x through 536ae683ff6; svn era, no PR) wrote the isWithin line 'Math.abs(z - this.sinLatitude) < 1e-10', replacing the exact 'z == sinLatitude' from the class's creation 400c39bac77 on the same unreleased branch. The WGS84 half comes from 560f3a1a0104 (LUCENE-6487 'Add WGS84 capability to geo3d support', issue #7546; branch lucene6487 r1681907; trunk 77de0ed1cf8, branch_5x 8c3e6b0ed32), which moved the class's plane, interior point, bounds and intersects to the planet model but left isWithin on sin(latitude). [read: git blame -w on main gives the isWithin body from 1242bf1519c (55d46dc6d37 is a reformat) and the plane line from 560f3a1a010; exec: the one-line fix shadowed over 5.2.0, 5.3.0 and main removes every failure, so the oldest failures come from this line] (2015-04-22 (isWithin line, branch commit; merged to trunk and branch_5x 2015-05-06); 2015-05-27 (WGS84 plane change, branch commit; merged 2015-06-02))
- First affected version
- 5.2.0 [exec] (the first release with geo3d; 5.1.0 has no geo3d package, so the check does not compile there). On 5.2.x only the unit sphere exists, so the defect shows as the false CONTAINS and the prefix-tree false matches; the WGS84 false negatives start with 5.3.0 [exec] (planet models, LUCENE-6487)
- Affected versions
- 5.2.0 to 10.6.0 (10.6.0 by reading) and main a9779ef3c193. [exec] fails on 5.2.0, 5.2.1, 5.3.0, 5.5.5, 6.0.0 and 7.0.1 (version-adapted main() checks with the finder's inputs: SPHERE CONTAINS, 4 prefix-tree false hits against 0 for the control, and from 5.3.0 the WGS84 on-latitude rejection), on 7.7.3 (geometry checks plus the finder's prefix-tree JUnit, 1 of 1), and on 8.11.4, 9.12.3, 10.5.2 and main (geometry checks plus the finder's JUnits, 3 of 3); the independent reproduction's JUnits fail 5 of 5 on main (-ea and -da), 10.5.2 and 9.12.3; the security-reach JUnit fails as expected on main (-ea and -da), 10.5.2 and 9.12.3. Every control passed, and the one-line fix shadowed passes on 5.2.0, 5.3.0 and main. [read] all 96 release tags 5.2.0..10.5.2 in the history clone contain the line (8.8.2..8.11.4 are untagged there; 8.11.4 was executed); branch_10x, branch_10_6 and upstream main a95c754ee003 (2026-10-26) carry the same line. Not fixed upstream.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), 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 (2026-10-26, follow-up prior-report record verdict new, read-only: 178 queries, namely scripted prior-report search v2 (51: GitHub issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive, web), 74 GitHub GraphQL searches over apache/lucene, Elasticsearch, OpenSearch, Solr, lucene-solr, spatial4j and Lucene.NET including a sweep of the module:spatial3d label (107 issues, 27 PRs), 5 GitHub REST, 15 JIRA, 24 list and 8 web searches, and 31 threads read in full; plus the finder's scripted prior-report search v1 run, 51 queries, 359 distinct hits, 0 high). Closest, not this bug: #11544 / LUCENE-10508 (PRs #804, #824, #845, #855; 9.2.0) and #11708 / LUCENE-10673 (#1056; 9.4.0), constructor failures for near-equal latitudes whose fixes added and tuned GeoBBoxFactory.latitudesEquals, which routes boxes to this class, without touching it; #9016 / LUCENE-7967, a precision fix in the sibling GeoDegeneratePoint.isWithin; #8955 / LUCENE-7906 and #9036 / LUCENE-7988, open relationship issues about other shapes. Not fixed upstream: the file is the same blob on upstream main a95c754ee003, and branch_10x and branch_10_6 have the same line; none of the 407 open PRs touches GeoDegenerateLatitudeZone.java or GeoBBoxFactory.java.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): the two repro JUnits through gradle in a clean worktree, stock and with the fix, assertions on and off, plus the closest suites (TestGeoBBox,TestGeo3DPoint,org.apache.lucene.spatial.spatial4j.*) and, with the fix, all oflucene/spatial3dandlucene/spatial-extras(2026-10-28); the same JUnits through the hunt's classpath runner on main (-ea, -da, fix shadowed) and on the 10.5.2 release jars (-da). Hunt and history runs: the hunt's JUnits on main (-ea, -da), 10.5.2, 9.12.3, 8.11.4 and 7.7.3, and version-adapted checks on 5.2.0, 5.2.1, 5.3.0, 5.5.5, 6.0.0 and 7.0.1; some hunt reviews used mainea57360912ed, whose spatial3d and spatial-extras sources equala9779ef3c193.
- Finding record
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java:59 (at a9779ef3c193) (plane = new Plane(planetModel, sinLatitude)), :61 (interior point), :88-91 (isWithin: Math.abs(z - this.sinLatitude) < 1e-10)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBBoxFactory.java:59-69 (at a9779ef3c193) (full longitude and latitudesEquals(top, bottom) -> GeoDegenerateLatitudeZone)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Plane.java:88-91 (at a9779ef3c193) (latitude plane D uses the ellipsoid magnitude); Vector.java:26 (MINIMUM_RESOLUTION 1e-12)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseAreaShape.java:102-133 (at a9779ef3c193) (edge points inside and no intersection -> CONTAINS); GeoArea.java:47-64 (CONTAINS contract)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoWideDegenerateHorizontalLine.java:178-181 (at a9779ef3c193) (sibling isWithin uses plane.evaluateIsZero)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:132-155 (at a9779ef3c193) (newBoxQuery) and PointInShapeIntersectVisitor.java:80-95 (leaf test calls shape.isWithin)lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java:66-90 (at a9779ef3c193) (relate; CONTAINS -> WITHIN); prefix/tree/FilterCellIterator.java:48-51 (WITHIN cell indexed as a covered leaf); prefix/IntersectsPrefixTreeQuery.java:71; composite/IntersectsRPTVerifyQuery.java:226-246
- The finding in 30 seconds
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java:24, 38-39, 57-62, 88-91, 110-124, 144-147lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBBoxFactory.java:58-69, 137-154; Plane.java:88-91, 564-566; Vector.java:26, 29, 567-572; PlanetModel.java:35 (WGS84 axes; the 35.2-degree crossing computed by hand)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseAreaShape.java:51-99, 102-133; GeoRectangle.java:183; GeoArea.java:35-65lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:145-155; Geo3DUtil.java:173-189lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dShape.java:55-79; spatial4j/Geo3dShapeFactory.java:164-173; prefix/tree/QuadPrefixTree.java:335; prefix/IntersectsPrefixTreeQuery.java:70-75; composite/IntersectsRPTVerifyQuery.java:226-249; prefix/tree/FilterCellIterator.java:48-51
- Who is affected
lucene/spatial-extras/src/java/org/apache/lucene/spatial/spatial4j/Geo3dSpatialContextFactory.java:28, 33 (@lucene.experimental; default planet model SPHERE); lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBBoxFactory.java:22; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:42
- Why it looks unintended
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Membership.java:19-44; GeoBBoxFactory.java:22, 35; GeoDegenerateLatitudeZone.java:24-28, 38-39; GeoArea.java:34-35, 46-65; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java:142lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoWideDegenerateHorizontalLine.java:178-182; GeoDegenerateHorizontalLine.java:173-177; GeoDegenerateVerticalLine.java:170-174; GeoDegenerateLongitudeSlice.java:98-100 (grep: 1e-10 only in GeoDegenerateLatitudeZone.java:90)lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestGeoBBox.java:28-46; grep of lucene/*/src/test for GeoDegenerateLatitudeZone (no hit)
- Suggested fix
lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoDegenerateLatitudeZone.java:57-59, 88-91; Plane.java:88-91, 564-566
- TestGeoZeroHeightFullLongitudeBox.java (JUnit test):
repro/LFV-614/src/org/apache/lucene/spatial3d/geom/TestGeoZeroHeightFullLongitudeBox.java(29 lines)
Made in parallel with this doc; its probes ran on main a9779ef3c193 with assertions on and off, and with the fix.
assets/LFV-614/bug.png(524 KB)
