Twelve defects. Twelve fixes. Every single one measured against the real PROJ binary, not assumed away.
Today we released OxiProj 0.1.5 — a correctness release for the EPSG authority (proj.db) path, the part of OxiProj that turns an EPSG:xxxx code into a real coordinate reference system and datum transformation, as opposed to a hand-written +proj= string. The PROJ-string path was already faithful in every tested case; the authority path was not, and this release is the fix.
OxiProj is still exactly what it was on day one: a 100% Pure-Rust reimplementation of PROJ 9.8.0 — no proj-sys, no C toolchain, every crate #![forbid(unsafe_code)], compiles to a single static binary (or WASM) and runs everywhere. 0.1.5 doesn’t touch that foundation; it hardens the one code path that reads from a 9.5 MiB bundled SQLite-compatible EPSG database instead of parsing a proj-string directly — and the honesty here matters beyond OxiProj itself: oxigeo-proj 0.2.4 had already demoted Crs::from_epsg to an unregistered-code fallback because of these bugs. 0.1.5 is what lets a downstream consumer trust it again.
Why a “boring” correctness release is the one that matters most
A coordinate library doesn’t usually fail loudly. It fails by returning a plausible-looking number that’s quietly wrong — and every bug below shipped that way:
- An ellipsoid 3.28× too large.
Database::get_ellipsoidread the rawsemi_major_axiscolumn without checking its unit — EPSG:7007 “Clarke 1858” stores its axis in Clarke’s feet, not metres — soCrs::from_epsg(2314).to_proj_string()emitted+a=20926348where PROJ emits+a=6378293.64520876. - Cylindrical equal-area, equidistant-cylindrical, and Mercator-B projections off by
1/cos(lat_ts). The EPSG parameter “Latitude of 1st standard parallel” was mapped to+lat_1for every method, when five of the nine methods that declare it are cylindrical and need+lat_tsinstead — silently running atlat_ts=0. - 35 geodetic and 88 projected CRS silently placed on Greenwich. The authority path emitted no
+pmtoken at all, so every non-Greenwich-meridian datum (Lisbon, Paris, Bern, Jakarta, Ferro, Bogotá, Rome, Brussels, Stockholm, Athens, Oslo, plus Madrid and Paris RGS by decimal degrees) was projected as if it weren’t one. - Three real EPSG codes couldn’t build a transformer at all. Lambert Conic Conformal 1SP (EPSG method 9801) was rejected outright by an overly strict guard, so
EPSG:2062,EPSG:5469, andEPSG:24382failed withIllegalArgValueinstead of a coordinate. - A datum shift silently skipped on “the single most common transform direction in the library.” Pairing
EPSG:4326against any authority CRS on a different datum applied no shift at all — 166 m off onEPSG:2056, 124 m onEPSG:27700, 80 m onEPSG:2039. - Superseded operations still applied, and area-of-use ignored. A Helmert or grid operation could be used far outside the extent it’s valid for —
EPSG:4220at one point was 339 m off using a superseded operation PROJ itself no longer selects there.
OxiProj 0.1.5 ends all of that — twelve defects in total, five originally reported and seven more found during the audits that followed each fix, every one closed and pinned with a regression test that reproduces the oracle’s own number.
Technical Deep Dive: falsifying every fix before believing it
The methodology is the headline as much as the fixes: every number in the CHANGELOG was measured against Homebrew PROJ 9.7.0 (proj, cs2cs, projinfo, cct) — the same proj.db OxiProj bundles, verified byte-identical by SHA-256. Nothing here is derived from reading PROJ’s source; it’s read off the reference binary running on real coordinates.
The regression suite that came out of this is new and specific — authority_unit_fidelity.rs (20 tests), wgs84_hub_operations.rs (6), pm_datum_chains.rs (9), lcc_1sp.rs (11), mixed_pair_datum_composition.rs (16), operation_area_of_use_selection.rs (10), operation_fallback_policy.rs (10, each carrying the exact PROJ_DEBUG=3 cs2cs transcript that established the rule it encodes), molodensky_badekas.rs (9), and more. And each fix was falsified before being believed: disabling the chain route, the unified DMS decoder, or the Molodensky-Badekas pivot in turn was verified to turn a named set of tests red — nine tests red for the pivot alone — before the fix was trusted and shipped.
One more thing disclosed rather than hidden: building a Transformer on the authority path is materially slower in 0.1.5 than in 0.1.4 — twenty Transformer::new(EPSG:4267, EPSG:4326) calls (the widest candidate set in the bundled registry) cost 2.5 s of CPU on published 0.1.4 and 20.7 s on 0.1.5, because each retained candidate now resolves a real authority extent and recompiles a pipeline instead of skipping that work. It’s the direct cost of the correctness fixes above, and it’s called out in the CHANGELOG with a specific follow-up (operation_endpoints memoization) rather than left for someone else to discover.
Getting Started
cargo add oxiproj --features epsg
use oxiproj::{Crs, Transformer, Coordinate};
fn main() -> Result<(), oxiproj::TransformError> {
// WGS84 (lon, lat) -> EPSG:6933, Lambert Cylindrical Equal Area.
let wgs84 = Crs::wgs84()?;
let ease_grid = Crs::from_epsg(6933)?;
let t = Transformer::new(wgs84, ease_grid)?;
let input = Coordinate { x: -144.0, y: -68.8 };
let output = t.transform(&input)?;
println!("EPSG:6933: x = {}, y = {}", output.x, output.y);
// 0.1.5: x = -13894024.356129, y = -6841313.452864 (matches `cs2cs`)
// 0.1.4: x = -16030006.674231, y = -5929715.294180 (2.1 km off — the +lat_ts bug above)
Ok(())
}
What’s New in 0.1.5
- Fixed: ellipsoid axis unit conversion (Clarke’s feet / Indian feet → metres) for
Crs::from_epsgoutput and the computational ellipsoid alike - Fixed: method-specific EPSG-parameter-to-PROJ-key mapping (
+lat_1vs+lat_ts) across all nine affected projection methods - Fixed: duplicate
+units=/+to_meter=tokens in projected PROJ-string output that broke round-tripping - Fixed: 1e-14-level angular-parameter rounding, now mirroring PROJ’s own
Measure::convertToUnit - Fixed: prime-meridian token emission for 28 non-Greenwich datums (35 geodetic + 88 projected CRS)
- Fixed: Lambert Conic Conformal 1SP (EPSG method 9801) now builds via PROJ’s own canonical expansion
- Fixed: WGS84-hub datum composition for mixed authority/non-authority pairs — single Helmert row, chained (non-Greenwich) route, and Molodensky-Badekas row, all three shapes
- Fixed: per-point operation selection and EPSG supersession honored in both directions, now including gridless pairs and a projected CRS on either side
- Fixed: grid-operation direction (
inverse_direction, reverse grid-row selection, concatenated-member direction) - Fixed: PROJ’s ballpark-fallback selection policy, including per-point selection for a projected CRS on either side
- 2,855 tests passing (0 failing, 15
#[ignore]-skipped) with--all-features; 2,693 passing with default features - 0 clippy warnings;
cargo deny check bansclean; no newunwrap()/expect()in production code - Disclosed, not hidden: authority-path
Transformerconstruction is slower in 0.1.5 — the direct cost of the fixes above
Tips
- If you called
Crs::from_epsg/Transformer::from_epsgfor a non-metric-unit ellipsoid (Clarke’s feet, Indian feet — think old colonial-era CRS like EPSG:2314, EPSG:24382), your emitted axis values change from silently wrong to correct. Anything that cached the old numbers needs a refresh. - If you build a cylindrical equal-area, equidistant-cylindrical, or Mercator-variant-B CRS from an EPSG code, re-check your output — five of nine affected projection methods had the wrong parameter mapping until now.
- If you pair
EPSG:4326against an authority CRS on a different datum, re-verify against 0.1.5 — this was “the single most common transform direction in the library” and up to 166 m could go silently missing. Transformer::last_used_operation_wkt()no longer returnsNonebefore the first transform — it’s seeded at construction with the ranked-best candidate, so treatSome(name)as “what this transformer will use,” not “nothing has run yet.”- If you build
Transformers in a hot loop against wide candidate sets (NAD27 ↔ WGS84 is the worst case in the bundled registry), expect materially more CPU per construction in 0.1.5 — cache theTransformer, don’t rebuild it per point. - Eleven known divergences from PROJ 9.7.0 remain, every one measured and itemized in
CHANGELOG.md’s “Known divergences” list rather than swept under a tolerance — that list is also the authoritative source forTODO.md’s T3.7 backlog.
This is the foundation
0.1.5 leans on the same COOLJAPAN stack as always — oxisql-sqlite-compat for the C-free EPSG engine, oxih5 for GGXF grids, oxiblas for Helmert LSQ, oxicuda for GPU reprojection, oxiz for SMT-verified Helmert bounds, oxiarc-deflate/lzw for compressed grids, and oxihttp for optional network grid fetch — with the same north star: an EPSG authority path that OxiGeo (the renamed OxiGDAL) and every other downstream consumer can trust without a fallback.
Repository: https://github.com/cool-japan/oxiproj
Star the repo if you think “we measured it against the real binary” should be the minimum bar for a coordinate library, not a nice-to-have. The era of trusting a geospatial library’s silent output is over — Pure Rust means you get to verify every number yourself, and now every number in this release has been.
— KitaSan at COOLJAPAN OÜ August 18, 2026