Sometimes the most important fix in a release isn’t in the code — it’s in what the documentation says about the code.
Today we released OxiProj 0.1.4 — a maintenance, packaging, and documentation-accuracy release on top of 0.1.3. There is no new roadmap surface area here and no public API change. What shipped instead: two Python-packaging fixes, a corrected CLI example, dependency bumps across seven upstream COOLJAPAN crates, and a documentation pass that caught OxiProj’s own README understating its test coverage.
OxiProj is still exactly what it was on day one: a 100% Pure-Rust reimplementation of PROJ 9.8.0 — not a binding to libproj, no proj-sys, no C toolchain. Every crate is still #![forbid(unsafe_code)], wasm included. 0.1.4 doesn’t touch any of that; it fixes how OxiProj packages itself and how honestly it describes itself.
Why a “boring” release is worth reading
Dependency drift, packaging gaps, and stale documentation are the boring failure modes that quietly erode trust in a library long before a real bug does. 0.1.4 closes out four of them:
- The Python wheel shipped with no license text.
crates/oxiproj-py’spyproject.tomlnever listedLICENSE/LICENSE-THIRD-PARTYin maturin’sinclude, so the sdist and wheels built clean but carried no license file at all. Fixed: both files now sit beside the crate’s manifest and are explicitly included — Apache-2.0 for OxiProj itself, plus the MIT/X11 notice-preservation file for PROJ and GeographicLib, the two upstream projects OxiProj is a direct algorithmic port of. - PyPI publishing used a long-lived API token.
.github/workflows/pypi-publish.ymlnow publishes throughpypa/gh-action-pypi-publishunder apypideployment environment withid-token: write— OIDC trusted publishing instead of a static secret. The workflow also gained aworkflow_dispatchtrigger, so a failed publish can be re-run without re-tagging. - The flagship
cs2csexample produced garbage output. The CLI README’s Quick Start fedEPSG:4326lon,lat-first intocs2csunder the authority-declared-axis-order semantics that landed back in 0.1.2 — whereEPSG:4326is actually lat,lon. The “documented” WGS84 → UTM Zone 33N conversion was silently wrong. Corrected, and now verified against the PROJ 9.7.0 reference binary. - The README’s own coverage numbers were stale — in the pessimistic direction. More on that below; it’s the most interesting fix in this release.
Technical Deep Dive: auditing the audit
The differential-parity corpus (crates/oxiproj/tests/data/parity_corpus.txt) is OxiProj’s evidence that its projections match PROJ-C output at 1e-9° tolerance. README.md and TODO.md described it as 1009 records covering 149 of the 157 proj -l map projections, with eight named projections — omerc, nzmg, mod_krovak, s2, spilhaus, calcofi, som, lsat — flagged as outside the corpus, five of them “not yet regenerated.”
None of that was true of the file that actually shipped in 0.1.2. Re-verified for 0.1.4 against Homebrew PROJ 9.7.0: the committed corpus holds 1049 records and covers all 157 of 157 proj -l map projections, zero exclusions — every one of those eight “missing” projections was already present with real forward and inverse values. Regenerating the corpus from scratch reproduces the committed file byte-for-byte (SHA-256 aef3ba24…), and the live differential suite expands those 1049 records into 1972 OxiProj-vs-PROJ measurements: 203 families bit-faithful, 4 quarantined at 1e-9-or-tighter iteration residuals (eqc, imw_p, misrsom, som), 0 failing.
Two more corrections rode along with it: the reference oracle was mislabeled “Homebrew PROJ 9.8.0” in two places when the corpus header itself records PROJ 9.7.0 as what it was actually checked against (OxiProj still ports the 9.8.0 lineage — only the citation was wrong); and the downstream engine-cutover status still gated M16 as “blocked pending sign-off to modify the separate oxigdal project,” when that project renamed itself OxiGDAL → OxiGeo at its own v0.2.0 and the cutover is landed there — oxigeo-proj runs OxiProj as its default engine, ships without proj-sys, and keeps proj4rs only behind an opt-in compatibility feature.
Getting Started
Nothing changes in how you reach for OxiProj:
cargo add oxiproj
use oxiproj::{Crs, Transformer, Coordinate};
fn main() -> Result<(), oxiproj::TransformError> {
// WGS84 geographic (lon, lat in degrees) -> Web Mercator (meters).
let wgs84 = Crs::wgs84()?;
let webmerc = Crs::web_mercator()?;
let t = Transformer::new(wgs84, webmerc)?;
// GIS convention: x = longitude, y = latitude (degrees).
let input = Coordinate { x: 9.0, y: 48.0 };
let output = t.transform(&input)?;
println!("Web Mercator: x = {}, y = {}", output.x, output.y);
// Build a CRS directly from a proj-string, then transform into it.
let utm32 = Crs::from_proj("+proj=utm +zone=32 +datum=WGS84")?;
let to_utm = Transformer::new(Crs::wgs84()?, utm32)?;
let projected = to_utm.transform(&input)?;
println!("UTM 32N: x = {}, y = {}", projected.x, projected.y);
Ok(())
}
And from the CLI, the corrected example — note the axis order:
# Convert WGS84 → UTM Zone 33N (EPSG:4326 is lat,lon per authority axis order)
echo "52.52 13.41" | cs2cs EPSG:4326 EPSG:32633
What’s New in 0.1.4
- Added
crates/oxiproj-py/LICENSEandLICENSE-THIRD-PARTY, now packaged into the maturin sdist and wheels .github/workflows/pypi-publish.ymlswitched to PyPI trusted publishing (OIDC) with aworkflow_dispatchre-run triggeroxiarc-deflate/oxiarc-lzwupdated 0.4.0 → 0.4.1oxisql-sqlite-compat/oxisql-coreupdated 0.4.0 → 0.4.1oxihttpupdated 0.2.0 → 0.2.1oxih5updated 0.2.2 → 0.2.3oxiblasupdated 0.2 → 0.2.2oxifftupdated 0.4.1 → 0.4.2oxicudaupdated 0.5.3 → 0.5.5- Corrected the
cs2csQuick Start example’s axis order incrates/oxiproj-cli/README.md - Corrected README/TODO parity-corpus figures (1049 records, 157/157 projections, 0 exclusions), reference-oracle attribution (PROJ 9.7.0), and the OxiGDAL → OxiGeo downstream rename
- Decided (policy):
oxiproj-db’sgeopackagefeature stays opt-in for 0.1.4 — the bundledproj.dbalready serves EPSG lookups by default; revisit at 0.2 if there’s demand
Tips
- No action needed for most users. No breaking change, no behavior change —
cargo updatecovers it. - If you distribute the
oxiprojPython wheel, or build your own maturin package, this release is a worked example of the[tool.maturin] includefix: license files don’t ship automatically just because they sit in the repo — they need to be ininclude, next to the manifest maturin actually builds from. - If you publish to PyPI from GitHub Actions, copy the trusted-publishing pattern: a
pypienvironment,id-token: write, andpypa/gh-action-pypi-publishinstead of a stored token that can leak or expire. - If you scripted against the old
cs2csQuick Start example, double check your own axis order —EPSG:4326is lat,lon under PROJ’s authority-declared convention, and the old example silently had it backwards. - If you were holding off on OxiProj pending full projection coverage, the coverage was already there as of 0.1.2 — 157 of 157
proj -lprojections, 1049 corpus records, verified byte-identical on regeneration. The 0.1.4 fix was in what the docs claimed, not in what shipped. - If you’re tracking the OxiGDAL cutover, start looking for OxiGeo instead — same project, same landed integration, renamed at its 0.2.0.
This is the foundation
0.1.4 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: letting OxiGeo (the renamed OxiGDAL) run its coordinate engine fully C-free.
Repository: https://github.com/cool-japan/oxiproj
Star the repo if you think a library’s documentation should be held to the same accuracy bar as its code. The era of “close enough” changelog figures is over — Pure Rust means you get to verify everything, including what we say about it.
— KitaSan at COOLJAPAN OÜ August 17, 2026