wbprojection 0.1.0

Whitebox Projections is a map projection library for Rust, inspired by PROJ
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
# wbprojection

A map projection library for Rust, inspired by [PROJ](https://proj.org/), and intended to serve as the projection engine for [Whitebox](https://www.whiteboxgeo.com).

`wbprojection` provides **forward** and **inverse** transformations between geographic coordinates (longitude/latitude) and projected Cartesian coordinates (easting/northing) for a wide range of map projections. It also supports datum transformations via Helmert parameters, and includes a built-in registry of EPSG codes.

## Table of Contents

- [Mission](#mission)
- [The Whitebox Project](#the-whitebox-project)
- [Is wbprojection Only for Whitebox?](#is-wbprojection-only-for-whitebox)
- [What wbprojection Is Not](#what-wbprojection-is-not)
- [Features](#features)
- [Quick Start](#quick-start)
- [Using EPSG Codes](#using-epsg-codes)
- [Supported Datums](#supported-datums)
- [Supported Ellipsoids](#supported-ellipsoids)
- [Manual Projection Construction](#manual-projection-construction)
- [CRS-to-CRS Transformation](#crs-to-crs-transformation)
- [Batch Transformation](#batch-transformation)
- [Grid-Shift Datum Workflow (NTv2 / NADCON)](#grid-shift-datum-workflow-ntv2--nadcon)
- [Supported Projections](#supported-projections)
- [Error Handling](#error-handling)
- [Compilation Features](#compilation-features)
- [Performance](#performance)
- [Architecture](#architecture)
- [Known Limitations](#known-limitations)
- [License](#license)

## Mission

- Provide robust CRS and map projection support for Whitebox applications and workflows.
- Cover the most commonly needed EPSG coordinate reference systems in a built-in, dependency-free catalog.
- Keep all projection and datum math in Rust with minimal external dependencies.
- Prioritize standards compliance, PROJ-inspired API design, and predictable behavior.

## The Whitebox Project

Whitebox is a collection of related open-source geospatial data analysis software. The Whitebox project began in 2009 at the [University of Guelph](https://geg.uoguelph.ca), Canada, developed by [Dr. John Lindsay](https://jblindsay.github.io/ghrg/index.html) a professor of geomatics. Whitebox has long served as Dr. Lindsay's platform for disseminating the output of his geomatics-based research and has developed an extensive worldwide user base. In 2021 Dr. Lindsay and Anthony Francioni founded [Whitebox Geospatial Inc.](https://www.whiteboxgeo.com) in order to ensure the sustainable and ongoing development of this open-source geospatial project. We are currently working on the next iteration of the Whitebox software, **Whitebox Next Gen**. This crate is part of that larger effort.

## Is wbprojection Only for Whitebox?

No. `wbprojection` is developed primarily to support Whitebox, but it is not restricted to Whitebox projects.

- **Whitebox-first**: API and roadmap decisions prioritize Whitebox CRS needs.
- **General-purpose**: the crate is usable as a standalone map projection and CRS library in other Rust geospatial applications.
- **Interop-focused**: standards-compliant EPSG registry, WKT export/import, and datum transformation support make it suitable for broader tooling and data pipelines.

## What wbprojection Is Not

`wbprojection` is a CRS and map projection engine. It is **not** intended to be a complete geospatial processing framework.

- Not a replacement for PROJ (coverage is broad but not exhaustive; some exotic projection methods or datum models may not be supported).
- Not a complete WKT/GML parser (`from_wkt` handles common WKT patterns but is not a full OGC/EPSG WKT standards engine).
- Not a pipeline engine for arbitrary coordinate transformations beyond the CRS-to-CRS model.
- Not a datum grid provider (NTv2 / NADCON grid files must be supplied externally).

---

## Features

- **94 projection types** with full forward + inverse support
- **EPSG registry** — instantiate any supported CRS from a numeric code (`Crs::from_epsg(32632)`)
- **Ellipsoidal** formulations (not just spherical approximations)
- **Standard ellipsoid catalog** with name/EPSG ellipsoid-code lookup helpers
- **UTM convenience constructor** for all 60 zones, N and S
- **Datum transformations** across 28 built-in datums (Helmert + grid-shift capable)
- **Grid-shift datum support** with NTv2 (`.gsb`) and NADCON ASCII pair loaders
- **CRS-to-CRS pipelines**: transform directly between any two supported coordinate reference systems
- **Pure Rust**, no C dependencies
- **Zero required dependencies** (only `thiserror` for ergonomic error handling)
- Optional `serde` feature for serialization

---

## Quick Start

Add to `Cargo.toml`:

```toml
[dependencies]
wbprojection = "0.1"
```

---

## Using EPSG Codes

The easiest way to create a projection is by EPSG code. The built-in registry currently covers **5591 EPSG codes** (**5594 total CRS/projection codes**, including ESRI 54008, 54009, 54030) and requires no external database or network access.

```rust
use wbprojection::Crs;

// WGS84 / UTM zone 32N
let crs = Crs::from_epsg(32632)?;
let (easting, northing) = crs.forward(9.18, 48.78)?;
// → ~513298 m, ~5404253 m  (Stuttgart, Germany)
let (lon, lat) = crs.inverse(easting, northing)?;

// Web Mercator (used by Google Maps, OpenStreetMap)
let crs = Crs::from_epsg(3857)?;
let (x, y) = crs.forward(13.4, 52.5)?;  // Berlin

// British National Grid
let crs = Crs::from_epsg(27700)?;
let (e, n) = crs.forward(-0.1276, 51.5074)?;  // London

// WGS84 geographic (degrees in, metres out)
let crs = Crs::from_epsg(4326)?;

// French Lambert-93
let crs = Crs::from_epsg(2154)?;
let (x, y) = crs.forward(2.35, 48.85)?;  // Paris

// Netherlands RD New
let crs = Crs::from_epsg(28992)?;

// NAD83 / UTM zone 18N
let crs = Crs::from_epsg(26918)?;

// NAD27 / UTM zone 15N
let crs = Crs::from_epsg(26715)?;
```

The top-level `from_epsg` function is also available as a shorthand:

```rust
use wbprojection::from_epsg;

let crs = from_epsg(32755)?;  // WGS84 / UTM zone 55S (eastern Australia)
```

### Querying the registry

```rust
use wbprojection::epsg::{epsg_info, known_epsg_codes};

// Look up metadata without constructing the CRS
if let Some(info) = epsg_info(27700) {
    println!("{}: {} ({})", info.code, info.name, info.area_of_use);
    // → 27700: OSGB 1936 / British National Grid (UK)
}

// List every supported code
let codes = known_epsg_codes();
println!("{} EPSG codes supported", codes.len());
```

### ESRI WKT export

Generate an ESRI-formatted WKT string directly from an EPSG code:

```rust
use wbprojection::to_esri_wkt;

let wkt = to_esri_wkt(32632)?;
```

### OGC WKT export

Generate an OGC-formatted WKT string from an EPSG code:

```rust
use wbprojection::to_ogc_wkt;

let wkt = to_ogc_wkt(32632)?;
println!("{wkt}");
```

Swiss LV03/LV95 exports now use the explicit `Oblique_Stereographic` method name to match their EPSG method semantics rather than the generic stereographic label.

### Limited WKT import

`wbprojection` can now import WKT and SRS references when they embed an EPSG identifier:

```rust
use wbprojection::from_wkt;

let crs = from_wkt("GEOGCRS[\"WGS 84\",ID[\"EPSG\",4326]]")?;
assert!(crs.name.contains("WGS"));
```

When no EPSG identifier is embedded, `from_wkt` now falls back to an internal parser for common WKT1 and WKT2 `GEOGCS`/`GEOGCRS` and `PROJCS`/`PROJCRS` definitions. The importer is designed around the projection methods already supported by `ProjectionKind`, so it can ingest this crate's own WKT exports and many standard projected CRS definitions.

Compound CRS WKT can be parsed with `compound_from_wkt`:

```rust
use wbprojection::compound_from_wkt;

let compound = compound_from_wkt(
    "COMPOUNDCRS[\"Example\",GEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],ANGLEUNIT[\"degree\",0.0174532925199433]],VERTCRS[\"EGM96 height\",VDATUM[\"EGM96\"],CS[vertical,1],AXIS[\"gravity-related height\",up],LENGTHUNIT[\"metre\",1]]]"
)?;
println!("{}", compound.name);
```

Projected-unit factors in WKT (e.g., US survey foot) are respected for linear parameters such as false easting/northing during import.

Compound parsing is strict by design: `compound_from_wkt` expects exactly one horizontal component (`PROJCRS`/`PROJCS` or `GEOGCRS`/`GEOGCS`) and exactly one vertical component (`VERTCRS`/`VERT_CS`).
Nested compound trees are flattened recursively when they still resolve to exactly one horizontal and one vertical component; ambiguous trees with multiple horizontals or multiple verticals are rejected.

It is still not a complete standards-level WKT engine. Unsupported method names, uncommon unit models, and CRS forms outside the current projection surface will still return an `UnsupportedProjection` error.

### Adaptive EPSG identification for authority-missing WKT

For WKT strings that do not include explicit EPSG authority markers, `wbprojection`
now supports adaptive EPSG identification over all currently supported codes in
the built-in registry.

- `identify_epsg_from_wkt(...)` provides best-match identification in lenient mode.
- `identify_epsg_from_wkt_with_policy(...)` adds explicit match policy control.
- `identify_epsg_from_wkt_report(...)` returns scored top-candidate diagnostics.
- Equivalent CRS-based APIs are available:
    - `identify_epsg_from_crs(...)`
    - `identify_epsg_from_crs_with_policy(...)`
    - `identify_epsg_from_crs_report(...)`

```rust
use wbprojection::{
        EpsgIdentifyPolicy,
        identify_epsg_from_wkt_with_policy,
        identify_epsg_from_wkt_report,
};

let wkt = "PROJCS[\"NAD83_CSRS_UTM_zone_17N\",GEOGCS[\"GCS_NAD83(CSRS)\",DATUM[\"D_North_American_1983_CSRS\",SPHEROID[\"GRS_1980\",6378137,298.257222101]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",-81],PARAMETER[\"scale_factor\",0.9996],PARAMETER[\"false_easting\",500000],PARAMETER[\"false_northing\",0],UNIT[\"Meter\",1]]";

let epsg_lenient = identify_epsg_from_wkt_with_policy(wkt, EpsgIdentifyPolicy::Lenient);
let epsg_strict = identify_epsg_from_wkt_with_policy(wkt, EpsgIdentifyPolicy::Strict);
let report = identify_epsg_from_wkt_report(wkt, EpsgIdentifyPolicy::Lenient).unwrap();

assert_eq!(epsg_lenient, Some(2958));
assert_eq!(epsg_strict, None); // strict mode rejects ambiguous near-ties
assert!(report.ambiguous);
```

Policy behavior:

- `Lenient`: resolve best candidate when confidence threshold is met.
- `Strict`: require confident and unambiguous top candidate.

The candidate search is built from `known_epsg_codes()`, so identification
coverage automatically expands as additional EPSG entries are added to the
registry.

### Calibration and verification tools

This crate includes maintainer-only tools used to calibrate and verify WKT->EPSG
identification behavior. These sources now live under `dev/examples/internal/`
and are intentionally excluded from the published crate package:

- `epsg_identify_report` (single sample diagnostics)
- `epsg_identify_report_batch` (manifest-driven CSV diagnostics)
- `epsg_identify_verify_manifests` (CI-style pass/fail verification)
- `epsg_identify_manifest_template` (new profile manifest bootstrap)

These are development utilities for repository maintainers rather than part of
the public crate-facing example surface.

### GeoTIFF projection info

Generate GeoTIFF GeoKey information from an EPSG code:

```rust
use wbprojection::to_geotiff_info;

let info = to_geotiff_info(32632)?;
println!("model_type: {}", info.model_type);
println!("projected_cs_type: {:?}", info.projected_cs_type);
```

### Handling unsupported codes

If a code isn't in the registry, `from_epsg` returns an error rather than panicking:

```rust
match Crs::from_epsg(99999) {
    Err(ProjectionError::UnsupportedProjection(msg)) => {
        eprintln!("Not supported: {msg}");
        // Falls back to manual ProjectionParams construction
    }
    Ok(crs) => { /* use it */ }
    Err(e) => eprintln!("Other error: {e}"),
}
```

For opt-in fallback behavior, use policy-based resolution:

```rust
use wbprojection::{Crs, EpsgResolutionPolicy};

// Unknown code resolves to EPSG:4326
let crs = Crs::from_epsg_with_policy(
    99999,
    EpsgResolutionPolicy::FallbackToWgs84,
)?;

// Or choose an explicit fallback EPSG code
let web = Crs::from_epsg_with_policy(
    99999,
    EpsgResolutionPolicy::FallbackToEpsg(3857),
)?;

assert!(crs.name.contains("WGS"));
assert!(web.name.contains("Mercator"));
```

Available policies:

- `Strict` (default behavior)
- `FallbackToEpsg(code)`
- `FallbackToWgs84`
- `FallbackToWebMercator`

### 3D CRS Workflows (Geocentric + Vertical)

The library now supports 3D workflows for geocentric CRS and minimal vertical CRS handling.

Use `transform_to_3d` for strict 3D CRS transformations:

- Geographic/Projected <-> Geocentric: supported
- Vertical <-> Vertical: passthrough
- Vertical <-> Geographic/Projected: rejected (strict mode)
- Vertical <-> Geocentric: rejected

```rust
use wbprojection::Crs;

let geog = Crs::from_epsg(7843)?;      // GDA2020 geographic
let geoc = Crs::from_epsg(7842)?;      // GDA2020 geocentric (ECEF)

let (x, y, z) = geog.transform_to_3d(147.0, -35.0, 120.0, &geoc)?;
let (lon, lat, h) = geoc.transform_to_3d(x, y, z, &geog)?;
```

Use `transform_to_3d_preserve_horizontal` for explicit mixed vertical workflows where
horizontal context should be preserved as-is:

- Vertical <-> Geographic/Projected: returns `(x, y, z)` unchanged
- Vertical <-> Vertical: returns `(x, y, z)` unchanged
- Vertical <-> Geocentric: rejected

```rust
use wbprojection::Crs;

let vertical = Crs::from_epsg(7841)?;  // GDA2020 height
let utm = Crs::from_epsg(7846)?;       // GDA2020 / MGA zone 46

// Explicitly preserve horizontal coordinates while carrying height through.
let (x2, y2, z2) = utm.transform_to_3d_preserve_horizontal(
    500_000.0,
    6_120_000.0,
    42.0,
    &vertical,
)?;

assert_eq!((x2, y2, z2), (500_000.0, 6_120_000.0, 42.0));
```

This preserve-horizontal API is intentionally explicit so strict `transform_to_3d` remains
safe by default for ambiguous vertical/horizontal combinations.

If you have external vertical model offsets (for example, geoid undulation values), use:

- `transform_to_3d_preserve_horizontal_with_vertical_offsets(...)`
- `transform_to_3d_preserve_horizontal_with_vertical_offsets_and_policy(...)`
- `transform_to_3d_preserve_horizontal_with_provider(...)`
- `transform_to_3d_preserve_horizontal_with_provider_and_policy(...)`
- `ConstantVerticalOffsetProvider` for fixed-offset convenience
- `GridVerticalOffsetProvider` + `VerticalOffsetGrid` for native bilinear grid sampling

It applies:

- `h_ellps = z + source_to_ellipsoidal_m`
- `z_out = h_ellps - target_to_ellipsoidal_m`

This lets you keep horizontal coordinates unchanged while converting vertical reference
surfaces using offsets computed outside `wbprojection`.

Example provider-based workflow:

```rust
use wbprojection::{Crs, Result};

let vertical = Crs::from_epsg(7841)?;
let utm = Crs::from_epsg(7846)?;

let provider = |x: f64, y: f64, _src: &Crs, _dst: &Crs| -> Result<(f64, f64)> {
    // Replace with model lookup based on x/y and CRS context.
    let source_to_ellipsoidal = 30.0 + x * 0.0;
    let target_to_ellipsoidal = 10.0 + y * 0.0;
    Ok((source_to_ellipsoidal, target_to_ellipsoidal))
};

let (_x2, _y2, _z2) = utm.transform_to_3d_preserve_horizontal_with_provider(
    500_000.0,
    6_120_000.0,
    100.0,
    &vertical,
    &provider,
)?;
```

Example native grid-based workflow:

```rust
use wbprojection::{
    Crs,
    GridVerticalOffsetProvider,
    VerticalOffsetGrid,
    register_vertical_offset_grid,
};

register_vertical_offset_grid(VerticalOffsetGrid::new(
    "src_geoid",
    140.0,
    -40.0,
    10.0,
    10.0,
    2,
    2,
    vec![30.0, 30.0, 30.0, 30.0],
)?)?;

register_vertical_offset_grid(VerticalOffsetGrid::new(
    "dst_geoid",
    140.0,
    -40.0,
    10.0,
    10.0,
    2,
    2,
    vec![10.0, 10.0, 10.0, 10.0],
)?)?;

let geog = Crs::from_epsg(7843)?;
let vertical = Crs::from_epsg(7841)?;
let provider = GridVerticalOffsetProvider::new("src_geoid", "dst_geoid");

let (_x2, _y2, z2) = geog.transform_to_3d_preserve_horizontal_with_provider(
    147.0,
    -35.0,
    100.0,
    &vertical,
    &provider,
)?;

assert!((z2 - 120.0).abs() < 1e-9);
```

To load vertical offset grids from files, use the built-in loaders:

**ISG format** (International Service for the Geoid — EGM2008, EGM96, regional models):

```rust
use std::{fs::File, io::BufReader};
use wbprojection::{load_vertical_grid_from_isg, register_vertical_offset_grid};

let f = File::open("egm2008.isg")?;
let grid = load_vertical_grid_from_isg(BufReader::new(f), "egm2008")?;
register_vertical_offset_grid(grid)?;
```

**Simple header + data format** (manual or custom grids):

```rust
use std::{fs::File, io::BufReader};
use wbprojection::{load_vertical_grid_from_simple_header_grid, register_vertical_offset_grid};

let f = File::open("my_geoid.grid")?;
let grid = load_vertical_grid_from_simple_header_grid(BufReader::new(f), "my_geoid")?;
register_vertical_offset_grid(grid)?;
```

**GTX binary format** (PROJ/NOAA geoid grids):

```rust
use std::{fs::File, io::BufReader};
use wbprojection::{load_vertical_grid_from_gtx, register_vertical_offset_grid};

let f = File::open("geoid.gtx")?;
let grid = load_vertical_grid_from_gtx(BufReader::new(f), "geoid")?;
register_vertical_offset_grid(grid)?;
```

Note: for GeoTIFF-based vertical grids in the Whitebox stack, use `wbraster`
to read raster values and build/register a `VerticalOffsetGrid` in `wbprojection`.

Simple format layout (S-to-N rows, W-to-E columns):

```text
# my geoid grid
lon_min = 140.0
lat_min = -40.0
lon_step = 0.5
lat_step = 0.5
width = 3
height = 3
28.0 28.1 28.2
28.3 28.4 28.5
28.6 28.7 28.8
```

For legacy/vendor codes, use the built-in explicit alias catalog:

```rust
use wbprojection::{
    Crs,
    EpsgResolutionPolicy,
    epsg_alias_catalog,
    resolve_epsg_with_catalog,
};

// Inspect built-in alias mappings (examples include 900913, 102100 -> 3857)
let aliases = epsg_alias_catalog();
assert!(!aliases.is_empty());

// Resolve code using catalog first, then policy fallback
let resolved = resolve_epsg_with_catalog(900913, EpsgResolutionPolicy::Strict)?;
assert_eq!(resolved.resolved_code, 3857);
assert!(resolved.used_alias_catalog);

// Construct CRS directly from alias-enabled path
let web = Crs::from_epsg_with_catalog(102100, EpsgResolutionPolicy::Strict)?;
assert!(web.name.contains("Mercator"));
```

You can also register organization-specific aliases at runtime:

```rust
use wbprojection::{
    Crs,
    EpsgResolutionPolicy,
    clear_runtime_epsg_aliases,
    register_epsg_alias,
    unregister_epsg_alias,
};

clear_runtime_epsg_aliases();
register_epsg_alias(9100001, 4326)?; // local/vendor code -> WGS84

let crs = Crs::from_epsg_with_catalog(9100001, EpsgResolutionPolicy::Strict)?;
assert!(crs.name.contains("WGS"));

let _ = unregister_epsg_alias(9100001);
clear_runtime_epsg_aliases();
```

Quick API reference:

| API | Purpose | Notes |
|---|---|---|
| `Crs::from_epsg(code)` / `from_epsg(code)` | Strict lookup from built-in EPSG registry | Errors if unsupported |
| `Crs::from_epsg_with_policy(code, policy)` / `from_epsg_with_policy(...)` | Strict lookup with optional fallback policy | Does not use alias catalogs |
| `Crs::from_epsg_with_catalog(code, policy)` / `from_epsg_with_catalog(...)` | Catalog-aware lookup | Order: exact -> runtime alias -> built-in alias -> policy fallback |
| `resolve_epsg_with_policy(code, policy)` | Resolve code only (no CRS construction) | Returns `EpsgResolution` metadata |
| `resolve_epsg_with_catalog(code, policy)` | Resolve with alias catalogs | Indicates `used_alias_catalog` / `used_fallback` |
| `epsg_alias_catalog()` | Inspect built-in alias mappings | Static entries (legacy/vendor codes) |
| `register_epsg_alias(src, dst)` | Register runtime alias mapping | `dst` must be supported EPSG |
| `unregister_epsg_alias(src)` | Remove one runtime alias | Returns previous target if present |
| `runtime_epsg_aliases()` | List runtime aliases | Sorted `(source, target)` pairs |
| `clear_runtime_epsg_aliases()` | Clear runtime alias registry | Useful for test setup/teardown |

### EPSG codes covered

Currently supports **5591 EPSG codes** and **5594 total CRS/projection codes** (including ESRI 54008, 54009, 54030).

| Range / Code | Description |
|---|---|
| 4326, 4269, 4267, 4258, 4230, 4617 | Geographic 2D — WGS84, NAD83, NAD27, ETRS89, ED50, NAD83(CSRS) |
| 4283, 4148, 4152, 4167, 4189, 4619 | Geographic 2D — GDA94, Hartebeesthoek94, NAD83(HARN), NZGD2000, RGAF09, SIRGAS95 |
| 4681, 4483, 4624, 4284, 4322, 6318, 4615 | Geographic 2D — REGVEN, Mexico ITRF92, RGFG95, Pulkovo 1942, WGS 72, NAD83(2011), REGCAN95 |
| 4001–4016, 4018–4025, 4027–4029, 4031–4036, 4044–4047, 4052–4055 | Geographic 2D — additional legacy ellipsoid/datum definitions (Airy, Bessel, Clarke, Everest, GRS, Helmert, International, Hughes, authalic spheres) |
| 4026, 4037, 4038, 4048–4051, 4056–4063 | Additional projected systems — MOLDREF99 TM, WGS84 TMzn 35N/36N, RGRDC 2005 Congo TM zones and UTM 33S–35S |
| 3857 | Web Mercator (Google Maps, OpenStreetMap) |
| 3395, 4087, 32662 | World Mercator, World Equidistant Cylindrical, Plate Carrée |
| 3400, 3401, 3402, 3403, 3405, 3406 | Alberta 10-TM variants, VN-2000 UTM zones 48N and 49N |
| 3833, 3834, 3835, 3836, 3837, 3838, 3839, 3840, 3841, 3845, 3846, 3847, 3848, 3849, 3850, 3986, 3987, 3988, 3989, 3991, 3992, 3994, 3997 | Pulkovo and Katanga Gauss-Kruger zones, SWEREF99 RT90 emulation systems, Puerto Rico / St. Croix systems, Mercator 41, Dubai Local TM |
| 32601–32660 | WGS84 / UTM northern hemisphere (all 60 zones) |
| 32701–32760 | WGS84 / UTM southern hemisphere (all 60 zones) |
| 32201–32260, 32301–32360 | WGS72 / UTM north and south hemispheres (all 120 zones) |
| 32401–32460, 32501–32560 | WGS72BE / UTM north and south hemispheres (all 120 zones) |
| 2494–2758 | Pulkovo 1942/1995 Gauss-Kruger CM and 3-degree families (+ Samboja, LKS 1994, Tete outliers) |
| 2463–2491, 20004–20032, 28404–28432, 3329–3335, 4417, 4434, 5631, 5663–5665, 5670–5675 | Pulkovo 1995/1942 Gauss-Kruger 6-degree CM and zone families plus additional adjusted 1958/1983 GK variants |
| 2391–2396, 2400–2442 | Additional projected parity families: Finland GK zones, South Yemen GK zones, RT90 variant, and Beijing 1954 3-degree GK zone/CM systems |
| 2867–2888, 2891–2954 | Additional projected parity families: NAD83(HARN) StatePlane foot/intl-foot systems and mixed regional TM/GK/Mercator/CS63/NAD83(CSRS) MTM systems |
| 4120–4147, 4149–4151, 4153–4166, 4168–4176, 4178–4185 | Additional geographic parity block: legacy geographic datums from Greek/GGRS/KKJ through Madeira and related regional systems |
| 2172–2175 | Pulkovo 1942 Adj 1958 Poland zones II–V (double stereographic and TM) |
| 2188–2192, 2195–2198 | Azores/Madeira UTM systems, ED50 France EuroLambert, NAD83(HARN) UTM 2S, and ETRS89 Kp2000 variants |
| 2205–2213 | NAD83 Kentucky North, ED50 3-degree GK zones 9–15, and ETRS89 TM 30 NE |
| 2200–2204, 2214–2220, 2222–2226, 2228 | ATS77/REGVEN/NAD27/StatePlane and related UTM/LCC/TM families |
| 3580–3751 | NAD83/NSRS2007 StatePlane families, related UTM/HARN systems, and Reunion TM |
| 26901–26923 | NAD83 / UTM north zones 1–23 |
| 6328–6348 | NAD83(2011) / UTM north zones 59, 60, 1–19 |
| 26701–26722 | NAD27 / UTM north zones 1–22 |
| 2955–2962, 3154–3160, 3761, 9709, 9713, 22207–22222, 22307–22322, 22407–22422, 22607–22622, 22707–22722, 22807–22822 | NAD83(CSRS) and NAD83(CSRS) realization UTM north zones (active v1 and v2/v3/v4/v6/v7/v8 families) |
| 25801–25860 | ETRS89 / UTM north zones 1–60 |
| 23001–23060 | ED50 / UTM north zones 1–60 |
| 31965–31985, 6210, 6211, 5396 | SIRGAS 2000 / UTM zones 11N–24N and 17S–26S |
| 5463, 29168–29172, 29187–29195 | SAD69 / UTM zones 17N–22N and 17S–25S (active codes) |
| 24817–24821, 24877–24882 | PSAD56 / UTM zones 17N–21N and 17S–22S |
| 3034, 3035 | ETRS89 LCC Europe, LAEA Europe |
| 3031, 3032, 3413, 3976, 3995, 3996 | Antarctic/Arctic Polar Stereographic variants |
| 2163, 3408, 3409, 3410, 3571, 3572, 3573, 3574, 3575, 3576 | US National Atlas Equal Area, NSIDC EASE-Grid variants, North Pole LAEA regional variants |
| 3832 | WGS 84 / PDC Mercator |
| 6931, 6932, 6933 | NSIDC EASE-Grid 2.0 North/South/Global |
| 8857 | WGS 84 / Equal Earth Greenwich |
| 54008, 54009, 54030 | ESRI World Sinusoidal, Mollweide, Robinson |
| 6707, 6708, 6709 | RDN2008 / UTM zones 32N, 33N, 34N |
| 6732, 6733, 6734, 6735, 6736, 6737, 6738 | GDA94 / MGA zones 41, 42, 43, 44, 46, 47, 59 |
| 7849–7856 | GDA2020 / MGA zones 49–56 |
| 6784, 6786, 6788, 6790, 6800, 6802, 6812, 6814, 6816, 6818, 6820, 6822, 6824, 6826, 6828, 6830, 6832, 6834, 6836, 6838, 6844, 6846, 6848, 6850, 6856, 6858, 6860, 6862 | NAD83(CORS96)/NAD83(2011) Oregon local TM zones (metre) |
| 6870, 6875, 6876 | Albania TM 2010 and RDN2008 Italy TM systems |
| 6915, 6927 | South East Island 1943 / UTM zone 40N, SVY21 / Singapore TM |
| 6956, 6957 | VN-2000 / TM-3 zones 481 and 482 |
| 7257–7355 | NAD83(2011) / Indiana InGCS county systems (metre variants; ftUS variants included where defined) |
| 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454, 2455, 2456, 2457, 2458, 2459, 2460, 2461 | JGD2000 / Japan Plane Rectangular coordinate systems I–XIX |
| 6669, 6670, 6671, 6672, 6673, 6674, 6675, 6676, 6677, 6678, 6679, 6680, 6681, 6682, 6683, 6684, 6685, 6686, 6687, 6688, 6689, 6690, 6691, 6692 | JGD2011 / Japan Plane Rectangular coordinate systems I–XIX and UTM zones 51N–55N |
| 5514 | S-JTSK / Krovak East North |
| 27700, 29900 | British National Grid, Irish National Grid |
| 31466–31469 | German Gauss-Krüger zones 2–5 |
| 28992 | Netherlands RD New |
| 2154 | France Lambert-93 |
| 5070, 3577, 3578, 3579 | CONUS Albers Equal Area, Australian Albers, Yukon Albers |
| 28349–28356 | Australia GDA94 / MGA zones 49–56 |
| 32661, 32761 | WGS84 / UPS North, UPS South |
| 2229–2286, 26929–26998*, 2759–2866, 3465–3552, 6355–6627**, 3338 | US State Plane NAD83 — legacy + national meter SPCS83 coverage plus NAD83(HARN), NAD83(NSRS2007), and NAD83(2011) coverage (*excluding 26947, which is unassigned in EPSG; **excluding unassigned gaps 6357–6392 and 6622–6624) |
| 21781, 2056 | Swiss LV03, LV95 |
| 22275–22293 | South Africa Cape Lo projections |
| 3007–3014 | Sweden SWEREF99 local TM zones (12°–17.25°E) |
| 2176–2180 | Poland CS2000 zones 5–8 and CS92 (ETRS89) |
| 2100 | Greece GGRS87 / Greek Grid |
| 23700 | Hungary HD72 / EOV |
| 31700 | Romania Dealul Piscului 1970 / Stereo 70 |
| 3763 | Portugal ETRS89 / TM06 |
| 3765 | Croatia HTRS96 / TM |
| 3301 | Estonia ETRS89 / L-EST97 |
| 5243 | Germany ETRS89 / LCC (N) |
| 2039 | Israel 1993 / Israeli TM Grid |
| 3414 | Singapore SVY21 / TM |
| 2326 | Hong Kong 1980 Grid |
| 3347 | Canada NAD83 / Statistics Canada Lambert |
| 3978 | Canada NAD83 / Atlas Lambert |
| 3174 | NAD83 / Great Lakes and St Lawrence Albers |
| 6350 | NAD83(2011) / CONUS Albers |
| 3111 | Australia GDA94 / VicGrid |
| 3308 | Australia GDA94 / NSW Lambert |
| 7846–7848, 3812, 31256–31258, 31287, 5179, 5181, 5182, 5186, 5187, 3825, 3826, 3112, 3005, 3015, 3767, 2040–2043, 2046–2055, 2057, 2058–2061, 2063, 2064, 2067–2080, 2085–2098, 2105–2138, 2148–2153, 2158–2162, 2164–2170, 2397–2399 | Additional regional systems: Australia GDA2020 MGA 46–48, Belgium Lambert 2008, Austria MGI GK/Lambert, Korea KGD2002/Korean 1985 belts, Taiwan TWD97 TM2 zones, Australia GA Lambert, BC Albers, SWEREF99 18 45, Croatia UTM 33N/LCC, Cote d'Ivoire UTM, Hartebeesthoek94 Lo zones, Rassadiran Nakhl-e Taqi, ED50(ED77) UTM zones 38N-41N, Guinea UTM, Naparima UTM, ELD79 Libya TM/UTM zones, Carthage TM, Yemen NGN96 UTM, South Yemen GK, Hanoi GK, WGS72BE TM, Cuba Norte/Sur, NZGD2000 local circuits plus UTM 58S-60S, Accra grids, Quebec Lambert (CGQ77), NAD83(CSRS) UTM aliases, Sierra Leone grids/UTM, Luxembourg Gauss, MGI Slovenia Grid, and Pulkovo adjusted 3-degree GK zones |
| 7843, 7845, 5513, 2065, 31254, 31255, 31265–31267, 3766, 2048–2055, 2058, 31275, 31276 | Additional regional systems II: GDA2020 geographic/GA LCC, S-JTSK Krovak variants, Austria MGI GK and Balkans zones, Croatia LCC, Hartebeesthoek94 Lo19–Lo33, and ED50(ED77) UTM 38N |
| 7841, 7842 | GDA2020 vertical height CRS and geocentric CRS (ECEF XYZ) |

---

## Supported Datums

Built-in datum definitions currently include:

- `WGS84`
- `NAD83`
- `NAD83(CSRS)`
- `NAD27`
- `ETRS89`
- `ED50`
- `GDA94`
- `GDA2020`
- `CGCS2000`
- `SIRGAS2000`
- `New Beijing`
- `Xian 1980`
- `Antigua 1943`
- `Dominica 1945`
- `Grenada 1953`
- `Montserrat 1958`
- `St. Kitts 1955`
- `NZGD2000`
- `JGD2000`
- `JGD2011`
- `RDN2008`
- `VN2000`
- `OSGB36`
- `DHDN`
- `Pulkovo 1942(58)`
- `Pulkovo 1942(83)`
- `S-JTSK`
- `Belge 1972`
- `Amersfoort`
- `TM65`
- `Katanga 1955`
- `Cape`
- `Puerto Rico 1927`
- `St. Croix`
- `CH1903`
- `CH1903+`
- `South East Island 1943`
- `SVY21`

## Supported Ellipsoids

Built-in constants include:

- `WGS 84`
- `GRS 80`
- `Clarke 1866`
- `International 1924`
- `Bessel 1841`
- `Airy 1830`
- `Airy 1830 Modified`
- `Krassowsky 1940`
- `Clarke 1880 (RGS)`
- `IAU 1976`
- `Sphere`

Additional standard ellipsoids are available through lookup helpers:

- `Ellipsoid::from_name("WGS 72")`
- `Ellipsoid::from_name("GRS 67")`
- `Ellipsoid::from_name("Clarke 1880 (RGS)")`
- `Ellipsoid::from_name("Everest 1830")`
- `Ellipsoid::from_name("Helmert 1906")`
- `Ellipsoid::from_name("Australian National Spheroid")`
- `Ellipsoid::from_name("Fischer 1960")`

Common EPSG ellipsoid codes are also supported:

- `Ellipsoid::from_epsg_ellipsoid(7030)` → WGS 84
- `Ellipsoid::from_epsg_ellipsoid(7019)` → GRS 80
- `Ellipsoid::from_epsg_ellipsoid(7024)` → Krassowsky 1940
- `Ellipsoid::from_epsg_ellipsoid(7049)` → IAU 1976

## Manual Projection Construction

For projections or parameters not in the EPSG registry, you can build a `ProjectionParams` directly.

### UTM (manual)

```rust
use wbprojection::{Projection, ProjectionParams};

let proj = Projection::new(ProjectionParams::utm(32, false))?;
let (easting, northing) = proj.forward(9.18, 48.78)?;
```

### Lambert Conformal Conic

```rust
use wbprojection::{Projection, ProjectionParams, ProjectionKind};

let proj = Projection::new(
    ProjectionParams::new(ProjectionKind::LambertConformalConic {
        lat1: 33.0,
        lat2: Some(45.0),
    })
    .with_lat0(39.0)
    .with_lon0(-96.0),
)?;
let (x, y) = proj.forward(-96.0, 39.0)?;
```

### Custom ellipsoid

```rust
use wbprojection::{Projection, ProjectionParams, ProjectionKind, Ellipsoid};

let proj = Projection::new(
    ProjectionParams::new(ProjectionKind::TransverseMercator)
        .with_lon0(-2.0)
        .with_lat0(49.0)
        .with_scale(0.9996012717)
        .with_false_easting(400_000.0)
        .with_false_northing(-100_000.0)
        .with_ellipsoid(Ellipsoid::from_a_inv_f("Airy 1830", 6_377_563.396, 299.3249646)),
)?;
```

---

## CRS-to-CRS Transformation

Transform directly between any two CRSes. The pipeline automatically handles datum shifts through WGS84 as a pivot.

```rust
use wbprojection::Crs;

// Convert a UTM 32N coordinate into Web Mercator
let src = Crs::from_epsg(32632)?;
let dst = Crs::from_epsg(3857)?;

let (utm_e, utm_n) = src.forward(9.0, 48.0)?;
let (web_x, web_y) = src.transform_to(utm_e, utm_n, &dst)?;

// British National Grid → WGS84 geographic
let bng  = Crs::from_epsg(27700)?;
let wgs  = Crs::from_epsg(4326)?;
let (bng_e, bng_n) = bng.forward(-0.1276, 51.5074)?;
let (lon, lat) = bng.transform_to(bng_e, bng_n, &wgs)?;
```

### Policy-controlled transform behavior

Use `transform_to_with_policy` when you want explicit control over missing/out-of-extent grid-shift handling.

```rust
use wbprojection::{Crs, CrsTransformPolicy};

let src = Crs::from_epsg(4267)?; // NAD27 geographic
let dst = Crs::from_epsg(4326)?; // WGS84 geographic

// Strict (default): errors on missing/out-of-extent grid-shift transforms.
let strict = src.transform_to_with_policy(
    -96.0,
    41.0,
    &dst,
    CrsTransformPolicy::Strict,
)?;

// Fallback mode: if a grid-shift transform cannot be applied,
// it falls back to identity shift instead of returning an error.
let fallback = src.transform_to_with_policy(
    -96.0,
    41.0,
    &dst,
    CrsTransformPolicy::FallbackToIdentityGridShift,
)?;

println!("strict={strict:?}, fallback={fallback:?}");
```

### Transform trace metadata

Use `transform_to_with_trace` to get output coordinates plus which source/target
grid was selected during datum transformation:

```rust
use wbprojection::{Crs, CrsTransformPolicy};

let src = Crs::from_epsg(4267)?;
let dst = Crs::from_epsg(4326)?;

let trace = src.transform_to_with_trace(
    -96.0,
    41.0,
    &dst,
    CrsTransformPolicy::Strict,
)?;

// Equivalent convenience helper for strict mode:
let trace2 = src.transform_to_with_trace_strict(-96.0, 41.0, &dst)?;

println!(
    "x={}, y={}, src_grid={:?}, dst_grid={:?}",
    trace.x, trace.y, trace.source_grid, trace.target_grid
);
```

---

## Batch Transformation

```rust
use wbprojection::{Projection, ProjectionParams, ProjectionKind};
use wbprojection::transform::{CoordTransform, Point2D};

let proj = Projection::new(ProjectionParams::new(ProjectionKind::Mercator))?;
let mut points = vec![
    Point2D::lonlat(0.0, 0.0),
    Point2D::lonlat(10.0, 20.0),
    Point2D::lonlat(-45.0, -30.0),
];
// Transforms points in-place; returns a Vec<Result<()>> per point
let results = proj.transform_fwd_many(&mut points);
```

---

## Grid-Shift Datum Workflow (NTv2 / NADCON)

`wbprojection` can ingest shift grids and apply them through `DatumTransform::GridShift`.

### NTv2 (`.gsb`) example

```rust
use wbprojection::{Crs, Datum, Ellipsoid, ProjectionKind, ProjectionParams, register_ntv2_gsb};
use wbprojection::datum::DatumTransform;

// 1) Load + register a named NTv2 grid
register_ntv2_gsb("./grids/OSTN15_NTv2.gsb", "OSTN15")?;

// 2) Build a custom source CRS that uses that grid-shift datum
let osgb36_grid = Datum {
    name: "OSGB36 (grid)",
    ellipsoid: Ellipsoid::from_a_inv_f("Airy 1830", 6_377_563.396, 299.3249646),
    transform: DatumTransform::GridShift { grid_name: "OSTN15" },
};

let src = Crs::new(
    "OSGB36 geographic (grid)",
    osgb36_grid,
    ProjectionParams::new(ProjectionKind::Geographic),
)?;

// 3) Transform into WGS84 geographic
let wgs84 = Crs::from_epsg(4326)?;
let (lon_wgs84, lat_wgs84) = src.transform_to(-1.54, 53.80, &wgs84)?;
println!("{lon_wgs84:.8}, {lat_wgs84:.8}");
```

### NADCON ASCII pair example

```rust
use wbprojection::{
    Crs, Datum, Ellipsoid, ProjectionKind, ProjectionParams,
    register_nadcon_ascii_pair,
};
use wbprojection::datum::DatumTransform;

// Expects two ASCII files (lon-shift and lat-shift), both in arc-seconds.
// Header line format in each file:
// lon_min lat_min lon_step lat_step width height
register_nadcon_ascii_pair("./grids/conus.los", "./grids/conus.las", "NADCON_CONUS")?;

let nad27_grid = Datum {
    name: "NAD27 (grid)",
    ellipsoid: Ellipsoid::CLARKE1866,
    transform: DatumTransform::GridShift { grid_name: "NADCON_CONUS" },
};

let src = Crs::new(
    "NAD27 geographic (grid)",
    nad27_grid,
    ProjectionParams::new(ProjectionKind::Geographic),
)?;

let dst = Crs::from_epsg(4326)?;
let (lon_wgs84, lat_wgs84) = src.transform_to(-96.0, 41.0, &dst)?;
println!("{lon_wgs84:.8}, {lat_wgs84:.8}");
```

### Registry helpers

You can inspect and manage registered grids using:

- `has_grid(name)`
- `get_grid(name)`
- `unregister_grid(name)`

### NTv2 subgrid helpers

For multi-subgrid NTv2 files, you can now enumerate and target a specific subgrid:

- `list_ntv2_subgrids(path)`
- `load_ntv2_gsb_subgrid(path, grid_name, subgrid_name)`
- `register_ntv2_gsb_subgrid(path, grid_name, subgrid_name)`

To enable runtime coordinate-based subgrid selection (deepest covering subgrid),
register the full NTv2 hierarchy and use `DatumTransform::Ntv2Hierarchy`:

```rust
use wbprojection::{Crs, Datum, Ellipsoid, ProjectionKind, ProjectionParams, register_ntv2_gsb_hierarchy};
use wbprojection::datum::DatumTransform;

register_ntv2_gsb_hierarchy("./grids/my_ntv2.gsb", "MY_NTV2_DATASET")?;

let src = Crs::new(
    "Hierarchical NTv2 datum",
    Datum {
        name: "Hierarchical NTv2 datum",
        ellipsoid: Ellipsoid::WGS84,
        transform: DatumTransform::Ntv2Hierarchy {
            dataset_name: "MY_NTV2_DATASET",
        },
    },
    ProjectionParams::new(ProjectionKind::Geographic),
)?;
```

For debugging/audit logs, you can inspect which hierarchy entry would be selected:

- `resolve_ntv2_hierarchy_grid_name(dataset_name, lon_deg, lat_deg)`
- `resolve_ntv2_hierarchy_subgrid(dataset_name, lon_deg, lat_deg)`

You can also promote any built-in datum to a grid-backed transform using helper builders:

```rust
use wbprojection::Datum;

let osgb36_ntv2 = Datum::OSGB36.with_ntv2_hierarchy("OSTN15_DATASET");
let nad27_grid = Datum::NAD27.with_grid_shift("NADCON_CONUS");
```

### Real-world checklist

Before using a grid-shift transform in production, verify:

- **Correct source/target datums**: the grid must match the exact datum pair you intend to transform.
- **Grid coverage**: your coordinates fall inside the grid extent (outside points return a datum error).
- **Axis/units consistency**: longitudes and latitudes are geographic degrees at the `Crs::transform_to` API boundary.
- **Expected direction**: use a known checkpoint to confirm forward behavior and inverse round-trip.
- **Fallback policy**: decide whether your app should fail hard on missing grids or switch to Helmert-based fallback.
- **QA tolerance**: compare against authoritative reference values for your area of use and record acceptable error thresholds.

---


## Supported Projections

`ProjectionKind` currently supports **94 projection types** (human-readable names returned by `Projection::name()`):

| Projection type | Projection type | Projection type |
|---|---|---|
| Geographic | Geocentric | Vertical |
| Geostationary Satellite View | Mercator | Web Mercator |
| Transverse Mercator | Transverse Mercator South Orientated | UTM |
| Lambert Conformal Conic | Polar Stereographic (North) | Polar Stereographic (South) |
| Albers Equal-Area Conic | Azimuthal Equidistant | Lambert Azimuthal Equal-Area |
| Krovak | Hotine Oblique Mercator | Central Conic |
| Lagrange | Loximuthal | Euler |
| Tissot | Murdoch I | Murdoch II |
| Murdoch III | Perspective Conic | Vitkovsky I |
| Tobler-Mercator | Winkel II | Kavrayskiy V |
| Stereographic | Oblique Stereographic | Orthographic |
| Sinusoidal | Mollweide | McBryde-Thomas Flat-Pole Sine (No. 2) |
| McBryde-Thomas Flat-Polar Sine (No. 1) | McBryde-Thomas Flat-Polar Parabolic | McBryde-Thomas Flat-Polar Quartic |
| Nell | Equal Earth | Cylindrical Equal Area |
| Equirectangular | Robinson | Gnomonic |
| Aitoff | Van der Grinten | Winkel Tripel |
| Hammer | Hatano | Eckert I |
| Eckert II | Eckert III | Eckert IV |
| Eckert V | Miller Cylindrical | Gall Stereographic |
| Gall-Peters | Behrmann | Hobo-Dyer |
| Wagner I | Wagner II | Wagner III |
| Wagner IV | Wagner V | Natural Earth |
| Natural Earth II | Wagner VI | Eckert VI |
| Transverse Cylindrical Equal Area | Polyconic | Cassini-Soldner |
| Bonne | Bonne South Orientated | Craster |
| Putnins P4' | Fahey | Times |
| Patterson | Putnins P3 | Putnins P3' |
| Putnins P5 | Putnins P5' | Putnins P1 |
| Putnins P2 | Putnins P6 | Putnins P6' |
| Quartic Authalic | Foucaut | Winkel I |
| Werenskiold I | Collignon | Nell-Hammer |
| Kavrayskiy VII | Two-Point Equidistant |  |

## Error Handling

All projection functions return `wbprojection::Result<T>` (an alias for `Result<T, ProjectionError>`).

```rust
use wbprojection::ProjectionError;

match crs.forward(0.0, 91.0) {
    Err(ProjectionError::OutOfBounds(msg))           => eprintln!("Out of bounds: {msg}"),
    Err(ProjectionError::ConvergenceFailure { iterations }) => eprintln!("Did not converge after {iterations} iterations"),
    Err(ProjectionError::UnsupportedProjection(msg)) => eprintln!("Unknown EPSG code: {msg}"),
    Err(e)                                           => eprintln!("Error: {e}"),
    Ok((x, y))                                       => println!("{x:.2}, {y:.2}"),
}
```

See also: [SIMD guardrail check](../../README.md#simd-guardrail-check) for a script you can run locally to verify speedup and correctness.

---

## Performance

This library uses the [`wide`](https://github.com/Lokathor/wide) crate to provide **SIMD optimizations** for datum-transformation kernels, with the current focus on batched Helmert ECEF operations (`HelmertParams::apply_simd_batch4` and `HelmertParams::apply_inverse_simd_batch4`). The public batch CRS APIs are available for batched workflows, but they currently still orchestrate per-point CRS transformations around those lower-level kernels rather than vectorizing the entire CRS pipeline end to end.

SIMD is **enabled by default** in this crate. There is currently no feature flag required to turn SIMD paths on.

This is a **temporary implementation strategy** until [Portable SIMD](https://github.com/rust-lang/rfcs/blob/master/text/2948-portable-simd.md) stabilizes in Rust. Once portable SIMD is available in stable Rust, `wbprojection` will transparently migrate to that standard approach while maintaining the same performance characteristics.

You can run the current benchmark example with:

```bash
cargo run --release --example simd_batch_transform
```

That example compares the scalar Helmert kernel against the SIMD batch kernel and also reports timings for the current CRS batch wrapper.

---

## Architecture

```
wbprojection/
├── lib.rs               – public API re-exports, helpers
├── ellipsoid.rs         – Ellipsoid struct + named constants
├── datum.rs             – Datum, HelmertParams, ECEF conversions
├── error.rs             – ProjectionError enum
├── transform.rs         – Point2D, Point3D, CoordTransform trait
├── epsg.rs              – EPSG registry (5591 EPSG codes / 5594 total CRS/projection codes → ProjectionParams)
├── epsg_generated_wkt.rs – Consolidated generated ESRI WKT lookup for EPSG entries
├── crs.rs               – Crs struct with datum + projection + from_epsg
└── projections/
    ├── mod.rs           – Projection, ProjectionParams, ProjectionKind
    ├── mercator.rs
    ├── transverse_mercator.rs
    ├── lambert_conformal_conic.rs
    ├── albers.rs
    ├── axis_oriented.rs
    ├── azimuthal_equidistant.rs
    ├── behrmann.rs
    ├── bonne.rs
    ├── cassini.rs
    ├── collignon.rs
    ├── craster.rs
    ├── eckert_i.rs
    ├── eckert_ii.rs
    ├── eckert_iii.rs
    ├── eckert_iv.rs
    ├── eckert_v.rs
    ├── eckert_vi.rs
    ├── fahey.rs
    ├── gall_stereographic.rs
    ├── krovak.rs
    ├── stereographic.rs
    ├── polar_stereographic.rs
    ├── orthographic.rs
    ├── sinusoidal.rs
    ├── mollweide.rs
    ├── equal_earth.rs
    ├── cylindrical_equal_area.rs
    ├── equirectangular.rs
    ├── geostationary.rs
    ├── robinson.rs
    ├── gnomonic.rs
    ├── hammer.rs
    ├── miller_cylindrical.rs
    ├── kavrayskiy_vii.rs
    ├── aitoff.rs
    ├── gall_peters.rs
    ├── hobo_dyer.rs
    ├── natural_earth.rs
    ├── patterson.rs
    ├── polyconic.rs
    ├── two_point_equidistant.rs
    ├── putnins_p3.rs
    ├── putnins_p3p.rs
    ├── putnins_p4p.rs
    ├── putnins_p5.rs
    ├── putnins_p5p.rs
    ├── putnins_p1.rs
    ├── times.rs
    ├── van_der_grinten.rs
    ├── wagner_ii.rs
    ├── wagner_iv.rs
    ├── wagner_v.rs
    ├── wagner_vi.rs
    ├── werenskiold_i.rs
    └── winkel_tripel.rs
```

---

## Compilation Features

`wbprojection` has minimal optional features.

| Feature | Default | Purpose |
|---------|:-------:|---------|
| `serde` | no | Enables `serde` `Serialize`/`Deserialize` for core CRS types. |

Example:

```toml
[dependencies]
wbprojection = { version = "0.1", features = ["serde"] }
```

The core projection and datum logic, EPSG registry, WKT export/import, and SIMD Helmert batch paths are always enabled and require no feature flag.

## Known Limitations

- Coverage is broad but not exhaustive; some exotic projection methods, datum models, or regional CRS variants may not be supported.
- `from_wkt` and `compound_from_wkt` handle the most common WKT patterns but are not a complete OGC/EPSG WKT standards engine; unsupported method names or uncommon unit models return `UnsupportedProjection`.
- Grid-shift datum transforms (NTv2, NADCON) require externally supplied grid files; `wbprojection` does not bundle or download grids.
- Adaptive EPSG identification (`identify_epsg_from_wkt`) searches the full registry on each call; prefer cached or explicitly known EPSG codes in performance-sensitive paths.
- `transform_to_3d` is strict about valid horizontal/vertical CRS component combinations; mixing unsupported combinations returns an error rather than silently degrading.
- The SIMD Helmert batch path accelerates datum-transformation kernels but does not yet vectorize the full CRS-to-CRS pipeline end-to-end.

## License

Licensed under either of [Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.