mgrs-convert 0.1.0

Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS grid references, round-tripping exactly at every precision from 100 km to 1 um
Documentation
# mgrs-convert

Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS grid
references, forward and reverse. UTM covers latitudes -80 to 84 and UPS covers
the polar caps, so the whole globe has a representation. MGRS precision runs
from 0 digit pairs, a 100 km cell, through 11, a 1 um cell, and every valid
reference names a cell with a well defined center. Zero runtime dependencies.

```toml
[dependencies]
mgrs-convert = "0.1.0"
```

## Use

```rust
use mgrs_convert::{LatLon, Mgrs, Precision};

let position = LatLon::new(40.748333, -73.985278)?;
assert_eq!(position.to_mgrs(Precision::ONE_METER).to_string(), "18TWL8566411315");

let cell: Mgrs = "18TWL8566411315".parse()?;
let center = cell.to_latlon();
assert!((center.lat() - 40.748333).abs() < 1e-5);
# Ok::<(), mgrs_convert::Error>(())
```

Four types, one error enum, nothing else. `LatLon` and `UtmUps` are
constructor-validated coordinates. `Mgrs` is a grid cell whose `Display` and
`FromStr` are the string round trip, and `Precision` is the validated
digit-pair count. `LatLon::to_mgrs` is infallible. `UtmUps::to_mgrs` returns
`Err(OutsideMgrsExtent)` for coordinates in the transfer slop outside the grid,
because the accepted transfer ranges extend 100 km past the MGRS coverage on
every side.

## Accuracy

The projection core is the Krueger series to order n^6 from Karney,
"Transverse Mercator with an accuracy of a few nanometers" (J. Geodesy 85(8),
2011). The grid follows NGA TM 8358.1. Differentially tested at build time
against GeographicLib's `GeoConvert` and its extended-precision `TMcoords.dat`
set: worst observed forward and reverse errors are under 5 nm, the round trip
through the crate closes under 4 nm, and MGRS strings match `GeoConvert`
exactly at every precision across 100,000 random points, the full 100 km
letter lattice, and both polar grids. The checked-in test suite pins those
vectors.

## Boundary behavior

- Latitude 0 and `-0.0` are identical on every path: northern, band N (the
  first recorded divergence: `GeoConvert` reads the sign of `-0.0` and formats
  the southern equator cell).
- The UTM latitude interval is [-80, 84), matching `GeoConvert`: -80 exactly
  is UTM band C, 84 exactly is UPS north, and the last UTM latitude is one ulp
  below 84 (band X).
- A coordinate exactly on an excluded upper grid edge is nudged inside by
  2^-28 m before truncation, so it formats as the last cell of its own square.
- Parsing folds case, trims outer ASCII whitespace, accepts a leading zero in
  the zone, and derives precision from the digit count. Grid-zone-only
  references such as `"18T"` are rejected with a named error (the second
  recorded divergence: `GeoConvert` accepts them).
- The Norway and Svalbard zone exceptions apply, pinned by tests.

## Migrating from `geoconvert`

This crate replaces `geoconvert` 1.0, which panics or returns wrong coordinates
on several of these paths (precision 8-11 parses, the polar formatting ring,
precision validation, upper-edge formatting, latitude 0).

| `geoconvert` call | replacement |
| --- | --- |
| `LatLon::create(lat, lon)` | `LatLon::new(lat, lon)` |
| `latlon.to_mgrs(6)` | `latlon.to_mgrs(Precision::new(6)?)` |
| `Mgrs::parse_str(s)` | `s.parse::<Mgrs>()` |
| `mgrs.to_string()` | unchanged |
| `mgrs.to_latlon()` | unchanged |
| `UtmUps::create(z, north, e, n)` | `UtmUps::utm(z, north, e, n)` or `UtmUps::ups(north, e, n)` |
| `utmups.to_mgrs(p)` | `utmups.to_mgrs(Precision::new(p)?)?` |
| `Mgrs::create(...)` | removed, construct via `to_mgrs` or `parse` |

The two new `?`s are the point: precision validation and the grid-extent check
now return errors instead of panicking.

## Scope

WGS84 only, since MGRS is defined on it. Datum transformations, zone forcing,
USNG datum suffixes, geodesic problems, and a CLI are all out of scope
(`geographiclib-rs` covers the geodesic problems well). The crate is `std`:
the projection core needs the `f64` math functions.

The declared minimum supported Rust version is 1.81. The CI floor job builds
the library and runs the doctests on 1.81.0 with the dev-dependencies table
stripped, a check anyone can rerun from the manifest.

## License

MIT or Apache-2.0, at your option.