Skip to content

Geodetic (lon/lat) R-tree with great-circle queries#233

Open
urschrei wants to merge 6 commits into
masterfrom
shugel/geodetic_3d
Open

Geodetic (lon/lat) R-tree with great-circle queries#233
urschrei wants to merge 6 commits into
masterfrom
shugel/geodetic_3d

Conversation

@urschrei

@urschrei urschrei commented Jun 4, 2026

Copy link
Copy Markdown
Member

This PR Adds an optional geodetic module: an R-tree over longitude/latitude data that
answers nearest-neighbour and radius queries, returning great-circle distance, for
points, linestrings, and polygons. The module is feature-gated and purely additive

What it is

GeodeticRTree<G> indexes GeodeticPoint, GeodeticLineString, and
GeodeticPolygon leaves (generic over an open GeodeticObject trait, so downstream
crates can supply their own). Queries take degrees in and return metres out; the
antimeridian and poles need no special handling.

How it works

Each (lon, lat) is mapped to a unit vector on the sphere (an n-vector) and stored in
a stock RTree with AABB<UnitVec> envelopes. Because the embedding is continuous
over the whole sphere, the ±180° seam and the poles are ordinary interior points requiring no
wrapping or splitting. Squared chord length (3-D Euclidean) is strictly monotonic in
great-circle angle, so the R*-tree's branch-and-bound traversal orders results by true
great-circle distance, and the box MINDIST is a sound lower bound – the unit-sphere
indexing method from a 2013 paper by Schubert, Zimek & Kriegel that proves the
lower-bound and monotonicity properties the pruning relies on. The same embedding also
underlies PostGIS geography, S2, and H3.

  • Extent leaves inflate each box to enclose the bulge of its great-circle arcs,
    with nearest-point-on-arc distance at the leaf; polygon membership is a great-circle
    ray cast with pole containment, so any simple polygon works, including ones larger
    than a hemisphere. Envelopes are precomputed and cached.
  • Earth model is spherical by default (GRS80 mean radius; ≤ ~0.5% vs an ellipsoid).
    The optional geodetic-wgs84 feature layers an exact ellipsoidal refine (Karney's
    geodesic, via geographiclib-rs) on top of the spherical index as a sound
    filter/refine, selectable per Ellipsoid (WGS84/GRS80/custom).

geodetic is no_std; geodetic-wgs84 requires std.

How it's tested

  • Property tests (Hegel) against brute-force oracles: NN and radius match a linear
    scan; the node envelope is a true MINDIST lower bound; arc boxes contain their arcs;
    point-to-geometry distance matches a cross-track/cap oracle.
  • Externally anchored arc maths: arc distances are checked against a 60-digit
    mpmath + s2sphere reference, with a curated golden set (ARC_GOLDEN). Because that
    ground truth is computed by external tools rather than by rstar's own code, the index
    can't pass a test by sharing the same mistake as the oracle it is checked against.
  • There are unit, edge-case, and end-to-end integration tests across points, linestrings, and
    polygons.

Headline benchmarks (M2 Pro)

Geodetic vs a naive planar RTree<[f64; 2]> over the same lon/lat data (100k-point
tree, criterion):

Operation Geodetic Planar Ratio
Bulk-load (2k points) 335 µs 274 µs 1.2×
Nearest-neighbour 2.1 µs/query 0.27 µs 7.7×
Radius (500 km) 13 µs/query 3.0 µs 4.4×

Reviewing

This is obviously a large feature. Where possible, I've tried to break up the commits logically, so they can (largely) be reviewed bottom-up, and individual module components have been kept relatively compact for ease of review.

LLM Use

I wrote this module in concert with Claude Opus 4.8, particularly the property tests, embedding, and distance calculations – I'm happy to answer any questions about the code.


  • I agree to follow the project's code of conduct.
  • I added an entry to rstar/CHANGELOG.md if knowledge of this change could be valuable to users.

@adamreichold

adamreichold commented Jun 5, 2026

Copy link
Copy Markdown
Member

My first reaction is not enthusiastic.

This is too much code to review at once and the likelihood of it eventually landing would be higher if it were split into more easily digestible parts.

Also considering

The module is feature-gated and purely additive

there is the question of whether this just should be a separate crate altogether.

Personally, I am certainly not keen on maintaining 7.500 lines of LLM-generated code which also pulls in a new test framework. (The sheer verbosity of it all is the most impressive property of it IMHO.)

I also think the rationale in the crate-level docs is too strongly worded IMHO. While the errors are untenable if one wants to index geometry over the whole globe, the just-treat-geodetic-like-cartesian-coordinates can work and is much simpler/more efficient for reasonably bounded regions like geometry within a single European state, i.e. just having geodetic coordinates at all is not automatically a reason to use an S^2-based index.

@urschrei

urschrei commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

Hmm I hadn't considered that it could "just" be a new crate. I'm not overly concerned about the maintenance burden (I wrote the module, so it's ultimately my responsibility).

The lack of stacked diffs makes the review process difficult, and I tried to mitigate that as much as possible with logical commits, almost all of which are independently reviewable.

That having been said, maybe it makes more sense as a standalone crate.

@adamreichold

Copy link
Copy Markdown
Member

Hmm I hadn't considered that it could "just" be a new crate. I'm not overly concerned about the maintenance burden (I wrote the module, so it's ultimately my responsibility).

I don't think it works like that. This is an ecosystem crate under joint maintenance by multiple people, so at least my expectation is that most of the maintainers understand most of the code. I also think that this is the signal this crate sends via its metadata and documentation.

If the goal really is an additive bus-factor-one module, then it should be a separate crate. Otherwise we need to work through the process were code merged into this repository is reviewed and understood by multiple maintainers which given the limited bandwidth most likely requires smaller easier-to-understand increments.

It is not just about what the code does or even how efficiently it does it, the main issue is that coding is theory building and FOSS is building shared theory building, so it is primarily about using code to teach people something.

@urschrei

urschrei commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

This is an ecosystem crate under joint maintenance by multiple people, so at least my expectation is that most of the maintainers understand most of the code.

That's fair enough.

In terms of the "core" code that needs to be thoroughly understood (though it will all have to be reviewed in some form), the footprint is a lot smaller (I estimate around 2500 LoC). The rest is almost all tests.

I'm open to any potential review strategy and/or re-arrangement of the commit series within reason. But I'm also open to a separate crate – it doesn't have the potential advantage you note above, of course.

urschrei added 6 commits June 5, 2026 21:15
…istance)

Add the `geodetic` feature and its embedding foundation: GeodeticCoord
(degrees, longitude first), the UnitVec n-vector embedding of (lon, lat) onto
the unit sphere, and the great-circle distance helpers (haversine_distance,
squared_chord, and the squared-chord/metre conversions), with inline unit and
Hegel property tests. The leaf types and the tree build on these; there is no
public tree yet.
Add GeodeticPoint (a lon/lat leaf) and GeodeticRTree, a great-circle spatial
index built on a stock RTree of unit vectors. Supports nearest-neighbour,
nearest-with-distance (metres), radius (locate_within_distance), exact-location,
and longitude/latitude window queries, with the antimeridian and poles handled
as ordinary interior points. The tree is generic over its leaf type via the
open GeodeticObject marker trait, defaulting to GeodeticPoint. Covered by
integration, property (Hegel), and embedding tests with a shared oracle module.
Add the great-circle arc primitives that extent leaves and custom leaf types
build on: arc_distance_2 (squared-chord distance from a point to a great-circle
arc), nearest_point_on_arc, arc_bounding_box (a unit-sphere box covering an
arc's bulge), and arc_contains_point. Validated by a property test whose oracle
and ARC_GOLDEN rows are anchored to an external 60-digit reference
(tests/reference/arc_distance_reference.py, cross-checked against s2sphere).
The GeodeticObject documentation now demonstrates a custom arc-based leaf.
Add two extent leaf types for the geodetic R-tree. GeodeticLineString is a
great-circle polyline with arc-aware bounding boxes (each box inflated to cover
its edges' bulge) and nearest-point distance. GeodeticPolygon (with
GeodeticRing) is a filled spherical polygon: membership is a great-circle ray
cast (zero distance inside, exact distance outside via robust orient3d
predicates), so any simple polygon is supported, including ones larger than a
hemisphere. Both index through the existing GeodeticObject leaf trait. Adds the
`robust` dependency for the polygon predicates, behind the `geodetic` feature,
and a property test suite anchored to the arc oracle.
Add the optional `geodetic-wgs84` feature: an ellipsoidal geodesic refine for
the point GeodeticRTree, layered on the spherical index as a filter/refine. The
reference ellipsoid is an Ellipsoid value (WGS84, GRS80, or a custom
Ellipsoid::new / from_inverse_flattening). nearest_neighbor_on_ellipsoid,
nearest_neighbor_with_distance_on_ellipsoid, and locate_within_distance_on_ellipsoid
re-rank or filter the spherical candidates by the exact geodesic distance
(Karney, via geographiclib-rs), with _wgs84-suffixed shorthands; geodesic_distance
is the standalone reference distance. The spherical lower bound keeps the
branch-and-bound search sound. The feature requires std; the base geodetic
feature stays no_std.
Add the geodetic benchmark suite (point, extent, and WGS84 benches) and update
rstar-benches to rand 0.10, dropping the geo-types dependency. Wire the
geodetic and geodetic-wgs84 features into CI (test, clippy with -D warnings,
and the no_std build), document the feature in both READMEs, and record the new
public API in the changelog.
@urschrei urschrei force-pushed the shugel/geodetic_3d branch from 1acb3a0 to 43bf657 Compare June 5, 2026 20:15
@adamreichold

Copy link
Copy Markdown
Member

I'm open to any potential review strategy and/or re-arrangement of the commit series within reason. But I'm also open to a separate crate – it doesn't have the potential advantage you note above, of course.

It is your work and hence your choice. If you want to get this out there for people to use with the minimum amount of fuzz, a separate crate is a reasonable choice IMHO. If you want it to become part of this project, you'll probably have to break it down a bit to take along the rest of us and teach us how it works.

Personally, I am interested in that and would like to invest some effort into it, but I understand that the amount of time you can spend on breaking things down is limited. So I am not asking for a slide deck, just a bare-bones version first without any of the bells and whistles and extra verification layers.

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.

Can I use rstar for geographic points & use a great circle distance?

2 participants