Repository navigation
Expand file tree
/
Copy pathhex9_c.cpp
More file actions
2103 lines (1940 loc) · 89.7 KB
/
Copy pathhex9_c.cpp
File metadata and controls
2103 lines (1940 loc) · 89.7 KB
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
/* hex9_c.cpp — C-ABI implementation for libhex9.
*
* Thin extern "C" wrappers over the C++ core (core/h9_*.h). Geometry-free:
* lon/lat degrees + 16-byte UUIDs in/out. See hex9_c.h for the contract.
*
* Include order mirrors the proven order from the PostGIS glue
* (lwgeom_hex9.cpp) so the warp shim resolves for h9_grid.h's cxcy path.
*/
#define H9_WARP_ENABLE 1
#include "h9_math.h"
#include "h9_addressing.h"
#include "h9_e4h.h"
#include "h9_uv_lattice.h"
#include "h9_grid.h"
#include "h9_kring.h"
#include "h9_curve.h"
#include "h9_cell_geom.h"
#include "h9_warp_runtime.h"
#include "hex9_c.h"
#include <cstring>
#include <cstddef>
#include <cstdio>
#include <cstdint>
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
/* Encoder mode: 1 = containment (grid-canonical, DEFAULT), 0 = NN (legacy).
* Containment is canonical: encode(point) then h9_bin matches h9_grid / h9_id /
* Python at EVERY layer including the L29 leaf; the NN beam encoder agrees only
* down to the leaf tail (they are bin-identical through ~L20, differ in ~3.7%
* of full UUIDs below — measured, tools/encoder_ab_probe). The PostGIS
* extension fixes this at the default (no GUC) so h9_encode stays truly
* IMMUTABLE; hex9_set_encoder exists only for embedders / A-B research (CLI,
* Python, probes). Read-only during a batch loop, so OpenMP-thread-safe. */
static int g_encoder_mode = 1;
/* Shared scalar kernels — the single source of truth for one item, called by
* both the scalar API and the batch loops (so batch == scalar by construction).
* Must stay reentrant: locals only, read-only warp state + g_encoder_mode. */
/* The ⟨face⟩→⟨cell⟩ seam: (cx, cy, oid) → full uuid via the grid-canonical
* containment descent, then (reclaimed layout) canonicalise to the mode-0
* home with the 3-bit tail via the identity round-trip, so the emitted uuid
* is exactly what bin(.,H9_LMAX) would give. L30 cells are sub-micron, so
* the round-trip is lossless in practice (stays well under 1 um). Shared by
* encode_one (canonical mode) and the bring-your-own-projection entry
* hex9_encode_boct — descent is projection-blind by construction. */
static inline void encode_from_cxcy(double cx, double cy, int oid,
uint8_t out[16]) {
h9grid::uuid_from_cxcy_full(cx, cy, oid, out);
#if !H9_HAS_HTERM
{
h9kring::H9CellId id;
if (h9kring::identity_from_uuid(out, H9_LMAX, &id))
h9kring::identity_to_uuid(id, H9_LMAX, out);
}
#endif
}
/* Same seam entered at the WARPED chart — the coordinates hex9_project /
* hex9_woct_to_boct emit (the public b_oct). Used by hex9_encode_boct:
* project → encode_boct is bit-identical to encode because both descend the
* identical h9_warp_inv output. */
static inline void encode_from_boct(double cx, double cy, int oid,
uint8_t out[16]) {
h9grid::uuid_from_boct_full(cx, cy, oid, out);
#if !H9_HAS_HTERM
{
h9kring::H9CellId id;
if (h9kring::identity_from_uuid(out, H9_LMAX, &id))
h9kring::identity_to_uuid(id, H9_LMAX, out);
}
#endif
}
static inline void encode_one(double lon, double lat, uint8_t out[16],
const H9Authalic *aux = &h9_g_authalic) {
if (g_encoder_mode == 1) {
double cx, cy; int oid;
h9grid::lonlatdeg_to_cxcy_oid(lon, lat, &cx, &cy, &oid, aux);
encode_from_cxcy(cx, cy, oid, out);
} else {
H9BOct b = h9_lonlatdeg_to_boct(lon, lat, aux);
h9_boct_to_uuid(b, out);
#if !H9_HAS_HTERM
/* Same canonicalisation for the legacy NN path (see encode_from_cxcy). */
{
h9kring::H9CellId id;
if (h9kring::identity_from_uuid(out, H9_LMAX, &id))
h9kring::identity_to_uuid(id, H9_LMAX, out);
}
#endif
}
}
static inline void decode_one(const uint8_t uuid[16], double *lon, double *lat,
const H9Authalic *aux = &h9_g_authalic) {
/* Bin UUIDs decode to the cell's geographic centroid via the exact
* identity path (grid convention — coherent with h9_grid/h9_cell/labels
* for every encoding flavour), which already realises the canonical
* terminal (p_mo=0, centroid t_cell). Legacy full UUIDs keep the beam
* backward walk (the representative point of the L29 address).
*
* Reclaimed layout: there is no h_term, so a max-depth UUID IS a bin — its
* deepest body nibble (H9_NIB_BODYTOP) is a real digit, not a sentinel.
* Route it through the SAME identity/centroid path so L30 decodes exactly
* like every shallower bin (no boct-walk special-case). */
uint8_t nib[32];
h9a_unpack(uuid, nib);
#if H9_HAS_HTERM
const bool is_bin = (nib[H9_NIB_TAIL - 1] == 0x0Fu);
#else
const bool is_bin = true;
#endif
if (is_bin) {
int bl = H9_NIB_BODYTOP;
while (bl >= 0 && nib[bl] == 0x0Fu) bl--;
h9kring::H9CellId id;
if (bl >= 0 && h9kring::identity_from_uuid(uuid, bl, &id) &&
h9cell::identity_centroid(id, bl, lon, lat, aux))
return;
/* fall through to the boct walk for malformed bins */
}
H9BOct b = h9_uuid_to_boct(uuid);
h9_boct_to_lonlatdeg(b, lon, lat, aux);
}
extern "C" const char *hex9_version(void) {
return "libhex9 " HEX9_VERSION " (" __DATE__ " " __TIME__ ")";
}
extern "C" int hex9_lmax(void) {
return H9_LMAX; /* 29 on HEX9_USE_L29 (legacy), 30 when reclaimed */
}
extern "C" int hex9_warp_init(char *errbuf, size_t errlen) {
std::string err;
/* The authalic series is the ellipsoid-specific half of the chain; the
* warp field is the other. Neither is optional — an address cannot be
* formed without both. */
if (!h9_authalic_ensure()) {
if (errbuf && errlen) {
std::strncpy(errbuf, "authalic series init failed", errlen - 1);
errbuf[errlen - 1] = '\0';
}
return 1;
}
const bool ok = h9::h9_warp_init_embedded(&err);
if (!ok && errbuf && errlen) {
std::strncpy(errbuf, err.c_str(), errlen - 1);
errbuf[errlen - 1] = '\0';
}
return ok ? 0 : 1;
}
/* Canonical name since 2.1.0: the function builds the whole addressing chain
* (authalic series + warp field), not just the warp. hex9_warp_init above is
* the kept-forever historic alias. */
extern "C" int hex9_init(char *errbuf, size_t errlen) {
return hex9_warp_init(errbuf, errlen);
}
extern "C" void hex9_set_use_warp(int on) { h9::g_warp_use = (on != 0); }
extern "C" void hex9_set_encoder(int mode) { g_encoder_mode = mode; }
extern "C" int hex9_encode(double lon, double lat, uint8_t out_uuid[16]) {
encode_one(lon, lat, out_uuid);
return 0;
}
/* Marker guards: E4H (0xE anywhere) and curve (nibble 0 = 0xC) uuids are
* different address kinds — the h9 machinery must reject them loudly, not
* resolve them into plausible-looking garbage (a curve uuid pushed through
* the boct walk decodes to the wrong hemisphere; found in the 2.3.0
* review). Curve input belongs to hex9_curve_*; E4H to hex9_e4h_*. */
static int h9_foreign_uuid(const uint8_t uuid[16]) {
return h9e4h_is_marked(uuid) || h9curve::is_curve_uuid(uuid);
}
extern "C" int hex9_decode(const uint8_t uuid[16], double *lon, double *lat) {
if (h9_foreign_uuid(uuid)) return 1; /* see h9_foreign_uuid */
decode_one(uuid, lon, lat);
return 0;
}
/* ── Sphere-datum twins ──────────────────────────────────────────────────────
* Identical chain minus the WGS84 authalic reduction: (lon, lat) are
* already-spherical degrees (h9_g_sphere, identity both ways). Same kernels,
* different aux — see hex9_c.h for the datum doctrine. */
extern "C" int hex9_encode_sphere(double lon, double lat, uint8_t out_uuid[16]) {
encode_one(lon, lat, out_uuid, &h9_g_sphere);
return 0;
}
extern "C" int hex9_decode_sphere(const uint8_t uuid[16], double *lon, double *lat) {
if (h9_foreign_uuid(uuid)) return 1; /* see h9_foreign_uuid */
decode_one(uuid, lon, lat, &h9_g_sphere);
return 0;
}
/* Canonical (grid-matching) bin at `layer` — the identity coarsening h9_grid
* enumerates with, which resolves a split-hex (6/7/8) leaf to its canonical
* MODE-0 parent (the cell that geometrically contains the point). h9_bin_uuid
* alone trusts the address's reversible MODE-1 nibble ancestry, so it bins a
* split-hex point into a cell that does NOT contain it (~20% of split-hex
* cells; grid-verified, e.g. L3 5038 vs the containing 5058). Going through
* identity makes hex9_bin == hex9_k_disk(.,0) == h9_grid everywhere — the
* bin's defining property (every point inside the cell maps to it uniquely)
* holds geometrically. The full UUID stays the reversible load-bearer; the bin
* is derived from it. L0 has no split ambiguity; an identity failure
* (malformed input, or a bin fed below its own layer) falls back to the
* legacy nibble bin. */
static void canonical_bin(const uint8_t uuid[16], int layer, uint8_t out[16]) {
if (layer == 0) {
/* L0 bin = the root hex itself (no split ambiguity), canonicalised to
* the mode-0 octant rep: keep nibble[0], sentinel the body, and write
* the canonical key_tail (c2_canon<<1, r_mo=p_mo=0). Matches Python
* h9_bin_pts' L0 branch; the general h9_bin_uuid path corrupts the root
* nibble at layer 0. Malformed root (>11) falls through to legacy. */
uint8_t nib[32];
h9a_unpack(uuid, nib);
if (nib[0] <= 11) {
uint8_t obn[32];
obn[0] = nib[0];
for (int i = 1; i <= H9_NIB_TAIL - 1; ++i) obn[i] = 0x0Fu;
obn[H9_NIB_TAIL] = (uint8_t)((H9_L0HEX_BACK[nib[0]][0][1] & 3u) << 1);
h9a_pack(obn, out);
return;
}
}
if (layer >= 1) {
h9kring::H9CellId id;
if (h9kring::identity_from_uuid(uuid, layer, &id)) {
h9kring::identity_to_uuid(id, layer, out);
return;
}
}
h9_bin_uuid(uuid, layer, out);
}
extern "C" int hex9_bin(const uint8_t uuid[16], int layer, uint8_t out_uuid[16]) {
if (layer < 0 || layer > H9_LMAX) return 1;
if (h9_foreign_uuid(uuid)) return 1; /* see h9_foreign_uuid */
canonical_bin(uuid, layer, out_uuid);
return 0;
}
/* ── Batch (OpenMP) ─────────────────────────────────────────────────────────
* Signed loop index for OpenMP portability (older specs require it). */
extern "C" int hex9_encode_many(const double *lon, const double *lat, size_t n,
uint8_t *out_uuid) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
encode_one(lon[i], lat[i], out_uuid + (size_t)i * 16);
return 0;
}
extern "C" int hex9_decode_many(const uint8_t *uuid, size_t n,
double *lon, double *lat) {
for (size_t i = 0; i < n; ++i)
if (h9_foreign_uuid(uuid + i * 16)) return 1;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
decode_one(uuid + (size_t)i * 16, &lon[i], &lat[i]);
return 0;
}
extern "C" int hex9_encode_many_sphere(const double *lon, const double *lat,
size_t n, uint8_t *out_uuid) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
encode_one(lon[i], lat[i], out_uuid + (size_t)i * 16, &h9_g_sphere);
return 0;
}
extern "C" int hex9_decode_many_sphere(const uint8_t *uuid, size_t n,
double *lon, double *lat) {
for (size_t i = 0; i < n; ++i)
if (h9_foreign_uuid(uuid + i * 16)) return 1;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
decode_one(uuid + (size_t)i * 16, &lon[i], &lat[i], &h9_g_sphere);
return 0;
}
extern "C" int hex9_bin_many(const uint8_t *uuid, int layer, size_t n,
uint8_t *out_uuid) {
if (layer < 0 || layer > H9_LMAX) return 1;
for (size_t i = 0; i < n; ++i)
if (h9_foreign_uuid(uuid + i * 16)) return 1;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
canonical_bin(uuid + (size_t)i * 16, layer, out_uuid + (size_t)i * 16);
return 0;
}
/* ── Canonical cell parent / ancestor (mode-0 convention) ─────────────────
* Thin surface over h9kring::h9_cell_parent_uuid / h9_cell_ancestor_uuid —
* see the doctrine comment there. */
extern "C" int hex9_cell_parent(const uint8_t uuid[16], uint8_t out_uuid[16]) {
if (h9e4h_is_marked(uuid)) return 1; /* E4H input: hex9_e4h_bin/split */
return h9kring::h9_cell_parent_uuid(uuid, out_uuid) ? 0 : 1;
}
extern "C" int hex9_cell_parent_many(const uint8_t *uuid, size_t n,
uint8_t *out_uuid) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= h9e4h_is_marked(uuid + (size_t)i * 16) ? 1
: (h9kring::h9_cell_parent_uuid(uuid + (size_t)i * 16,
out_uuid + (size_t)i * 16) ? 0 : 1);
return rc;
}
extern "C" int hex9_cell_ancestor(const uint8_t uuid[16], int layer,
uint8_t out_uuid[16]) {
if (h9e4h_is_marked(uuid)) return 1; /* E4H input: hex9_e4h_bin/split */
return h9kring::h9_cell_ancestor_uuid(uuid, layer, out_uuid) ? 0 : 1;
}
extern "C" int hex9_cell_ancestor_many(const uint8_t *uuid, int layer, size_t n,
uint8_t *out_uuid) {
if (layer < 0 || layer > H9_LMAX) return 1;
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= h9e4h_is_marked(uuid + (size_t)i * 16) ? 1
: (h9kring::h9_cell_ancestor_uuid(uuid + (size_t)i * 16, layer,
out_uuid + (size_t)i * 16) ? 0 : 1);
return rc;
}
/* ── Hamiltonian curve addressing ───────────────────────────────────────────
* Surface over core/h9_curve.h (36-state transducer; tables verbatim from
* hhg9). Input normalisation policy: a curve-uuid passes through; a
* canonical bin is trusted as-is (like the cell parent/ancestor family); a
* full/reversible uuid is re-binned canonically at its own depth through
* canonical_bin — the SAME path hex9_bin takes, so curve(encode(pt)) and
* curve(bin(encode(pt), L)) agree with h9_grid / Python at every layer. */
/* Shared kernel: uuid (h9 or curve) -> curve rows + layer. */
static int curve_rows_of(const uint8_t uuid[16], uint8_t *rows) {
if (h9e4h_is_marked(uuid)) return -1; /* E4H: no curve on tails */
if (h9curve::is_curve_uuid(uuid))
return h9curve::curve_unpack(uuid, rows);
bool rebin = false;
const int L = h9curve::curve_input_layer(uuid, &rebin);
if (L < 0) return -1;
uint8_t bin[16];
if (rebin)
canonical_bin(uuid, L, bin);
else
std::memcpy(bin, uuid, 16);
if (!h9curve::curve_rows_from_bin(bin, L, rows, nullptr)) return -1;
return L;
}
extern "C" int hex9_curve(const uint8_t uuid[16], uint8_t out_curve[16]) {
uint8_t rows[H9_NLEVELS];
const int L = curve_rows_of(uuid, rows);
if (L < 0) return 1;
h9curve::curve_pack_rows(rows, L, out_curve);
return 0;
}
extern "C" int hex9_curve_many(const uint8_t *uuid, size_t n, uint8_t *out_curve) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= hex9_curve(uuid + (size_t)i * 16, out_curve + (size_t)i * 16);
return rc;
}
extern "C" int hex9_curve_decode(const uint8_t curve[16], uint8_t out_uuid[16]) {
if (h9e4h_is_marked(curve)) return 1; /* E4H is neither curve nor h9 */
if (!h9curve::is_curve_uuid(curve)) { /* h9-uuid passes through */
if (h9curve::curve_input_layer(curve, nullptr) < 0) return 1;
std::memcpy(out_uuid, curve, 16);
return 0;
}
return h9curve::curve_decode_to_bin(curve, out_uuid) ? 0 : 1;
}
extern "C" int hex9_curve_decode_many(const uint8_t *curve, size_t n,
uint8_t *out_uuid) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= hex9_curve_decode(curve + (size_t)i * 16, out_uuid + (size_t)i * 16);
return rc;
}
extern "C" int hex9_is_curve(const uint8_t uuid[16]) {
return h9curve::is_curve_uuid(uuid) ? 1 : 0;
}
extern "C" int hex9_curve_layer(const uint8_t curve[16]) {
uint8_t rows[H9_NLEVELS];
if (!h9curve::is_curve_uuid(curve)) return -1;
return h9curve::curve_unpack(curve, rows);
}
extern "C" int hex9_curve_bin(const uint8_t curve[16], int layer,
uint8_t out_curve[16]) {
uint8_t rows[H9_NLEVELS];
if (!h9curve::is_curve_uuid(curve)) return 1;
const int L = h9curve::curve_unpack(curve, rows);
if (L < 0 || layer < 0 || layer > L) return 1;
h9curve::curve_pack_rows(rows, layer, out_curve);
return 0;
}
extern "C" int hex9_curve_index(const uint8_t uuid[16], char *buf, size_t buflen) {
uint8_t rows[H9_NLEVELS];
const int L = curve_rows_of(uuid, rows);
if (L < 0) return -1;
return h9curve::curve_index_decimal(rows, L, buf, buflen);
}
extern "C" int hex9_curve_pack(const char *index_dec, int layer,
uint8_t out_curve[16]) {
if (layer < 0 || layer > H9_LMAX) return 1;
uint8_t rows[H9_NLEVELS];
if (!h9curve::curve_rows_from_decimal(index_dec, layer, rows)) return 1;
h9curve::curve_pack_rows(rows, layer, out_curve);
return 0;
}
extern "C" int hex9_curve_label(const uint8_t uuid[16], char *buf, size_t buflen) {
uint8_t rows[H9_NLEVELS];
const int L = curve_rows_of(uuid, rows);
if (L < 0) return -1;
if (buflen < (size_t)L + 3) return -1; /* 'c' + slot + L ranks + NUL */
buf[0] = 'c';
buf[1] = (char)(rows[0] < 10 ? '0' + rows[0] : 'a' + rows[0] - 10);
for (int k = 1; k <= L; ++k) buf[1 + k] = (char)('0' + rows[k]);
buf[L + 2] = '\0';
return L + 2;
}
extern "C" int hex9_curve_parse_label(const char *label, uint8_t out_curve[16]) {
if (!label || label[0] != 'c' || !label[1]) return 1;
const char sc = label[1];
uint8_t rows[H9_NLEVELS];
if (sc >= '0' && sc <= '9') rows[0] = (uint8_t)(sc - '0');
else if (sc == 'a' || sc == 'b') rows[0] = (uint8_t)(sc - 'a' + 10);
else return 1;
int L = 0;
for (const char *p = label + 2; *p; ++p) {
if (*p < '0' || *p > '8' || L >= H9_LMAX) return 1;
rows[++L] = (uint8_t)(*p - '0');
}
h9curve::curve_pack_rows(rows, L, out_curve);
return 0;
}
extern "C" int64_t hex9_curve_ncells(int from_layer, int to_layer) {
if (from_layer < 0 || to_layer < from_layer || to_layer > H9_LMAX) return -1;
const int g = to_layer - from_layer;
if (g > 19) return -1; /* 9^20 overflows int64 */
int64_t n = 1;
for (int i = 0; i < g; ++i) n *= 9;
return n;
}
/* Shared input resolution for the enumeration family: h9 bin trusted,
* full uuid re-binned canonically, curve-uuid decoded. Returns the cell's
* own layer, or -1. */
static int curve_cell_input(const uint8_t uuid[16], uint8_t anc[16]) {
if (h9e4h_is_marked(uuid)) return -1; /* E4H: no lineage on tails */
if (h9curve::is_curve_uuid(uuid)) {
if (!h9curve::curve_decode_to_bin(uuid, anc)) return -1;
} else {
bool rebin = false;
const int L = h9curve::curve_input_layer(uuid, &rebin);
if (L < 0) return -1;
if (rebin) canonical_bin(uuid, L, anc);
else std::memcpy(anc, uuid, 16);
}
uint8_t nib[32];
h9a_unpack(anc, nib);
return h9curve::curve_body_layer(nib);
}
extern "C" int64_t hex9_curve_cells(const uint8_t uuid[16], int layer,
uint8_t *out_bins, uint8_t *out_curves,
int64_t max_cells) {
if (layer < 0 || layer > H9_LMAX || !out_bins) return -1;
uint8_t anc[16];
const int from = curve_cell_input(uuid, anc);
if (from < 0) return -1;
const int64_t want = hex9_curve_ncells(from, layer);
if (want < 0 || want > max_cells) return -1;
uint8_t rows[H9_NLEVELS];
int state = 0;
if (!h9curve::curve_rows_from_bin(anc, from, rows, &state)) return -1;
int64_t count = 0;
if (!h9curve::curve_cells_rec(anc, from, state, layer, rows,
out_bins, out_curves, &count))
return -1;
return count;
}
extern "C" int hex9_cell_children(const uint8_t uuid[16], uint8_t *out_uuids) {
if (!out_uuids) return 1;
uint8_t cell[16];
const int L = curve_cell_input(uuid, cell);
if (L < 0 || L >= H9_LMAX) return 1;
uint8_t rows[H9_NLEVELS];
int state = 0;
if (!h9curve::curve_rows_from_bin(cell, L, rows, &state)) return 1;
uint8_t kids[9][16];
if (!h9curve::curve_children_ranked(cell, L, state, kids)) return 1;
std::memcpy(out_uuids, kids, 9 * 16);
return 0;
}
extern "C" int64_t hex9_owned_cells(const uint8_t uuid[16], int layer,
uint8_t *out_bins, uint8_t *out_curves,
int64_t max_cells) {
if (layer < 0 || layer > H9_LMAX || !out_bins) return -1;
uint8_t zone[16];
const int from = curve_cell_input(uuid, zone);
if (from < 0 || layer < from) return -1;
return h9curve::curve_owned_cells(zone, from, layer,
out_bins, out_curves, max_cells);
}
/* ── Continuous projection (b_oct backend) ──────────────────────────────────
* Thin surface over the grid-canonical forward kernel (the same one encode_one
* uses in mode 1). No descent, no L29 cap — see hex9_c.h / boct_backend_design. */
extern "C" int hex9_project(double lon, double lat,
double *cx, double *cy, int *oid) {
h9grid::lonlatdeg_to_boct_oid(lon, lat, cx, cy, oid);
return 0;
}
extern "C" int hex9_project_many(const double *lon, const double *lat, size_t n,
double *cx, double *cy, int *oid) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::lonlatdeg_to_boct_oid(lon[i], lat[i], &cx[i], &cy[i], &oid[i]);
return 0;
}
extern "C" int hex9_unproject(double cx, double cy, int oid,
double *lon, double *lat) {
return h9grid::boct_oid_to_lonlat(cx, cy, oid, lon, lat) ? 0 : 1;
}
extern "C" int hex9_unproject_many(const double *cx, const double *cy,
const int *oid, size_t n,
double *lon, double *lat) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::boct_oid_to_lonlat(cx[i], cy[i], oid[i], &lon[i], &lat[i]);
return 0;
}
/* Sphere-datum twins of the projection surface (see hex9_encode_sphere). */
extern "C" int hex9_project_sphere(double lon, double lat,
double *cx, double *cy, int *oid) {
h9grid::lonlatdeg_to_boct_oid(lon, lat, cx, cy, oid, &h9_g_sphere);
return 0;
}
extern "C" int hex9_project_many_sphere(const double *lon, const double *lat,
size_t n,
double *cx, double *cy, int *oid) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::lonlatdeg_to_boct_oid(lon[i], lat[i], &cx[i], &cy[i], &oid[i],
&h9_g_sphere);
return 0;
}
extern "C" int hex9_unproject_sphere(double cx, double cy, int oid,
double *lon, double *lat) {
return h9grid::boct_oid_to_lonlat(cx, cy, oid, lon, lat, &h9_g_sphere) ? 0 : 1;
}
extern "C" int hex9_unproject_many_sphere(const double *cx, const double *cy,
const int *oid, size_t n,
double *lon, double *lat) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::boct_oid_to_lonlat(cx[i], cy[i], oid[i], &lon[i], &lat[i],
&h9_g_sphere);
return 0;
}
/* ── Warped octahedral cartesian CRS (w_oct; the 3D storage baseline) ────────
* Seamless 3D coordinate for the sphere — see hex9_c.h. xyz is on the unit
* octahedron; oid == sign(xyz). b_oct <-> xyz is pure rotation; lon/lat <-> xyz
* crosses the authalic warp (via hex9_project/unproject). */
extern "C" int hex9_boct_to_woct(double cx, double cy, int oid, double xyz[3]) {
if (oid < 0 || oid > 7) return 1;
h9grid::boct_to_woct(cx, cy, oid, xyz);
return 0;
}
extern "C" int hex9_woct_to_boct(const double xyz[3], double *cx, double *cy,
int *oid) {
h9grid::woct_to_boct(xyz, cx, cy, oid);
return 0;
}
extern "C" int hex9_to_woct(double lon, double lat, double xyz[3]) {
double cx, cy; int oid;
h9grid::lonlatdeg_to_boct_oid(lon, lat, &cx, &cy, &oid);
h9grid::boct_to_woct(cx, cy, oid, xyz);
return 0;
}
extern "C" int hex9_from_woct(const double xyz[3], double *lon, double *lat) {
double cx, cy; int oid;
h9grid::woct_to_boct(xyz, &cx, &cy, &oid);
return h9grid::boct_oid_to_lonlat(cx, cy, oid, lon, lat) ? 0 : 1;
}
extern "C" int hex9_boct_to_woct_many(const double *cx, const double *cy,
const int *oid, size_t n, double *xyz) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::boct_to_woct(cx[i], cy[i], oid[i], xyz + (size_t)i * 3);
return 0;
}
extern "C" int hex9_woct_to_boct_many(const double *xyz, size_t n,
double *cx, double *cy, int *oid) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i)
h9grid::woct_to_boct(xyz + (size_t)i * 3, &cx[i], &cy[i], &oid[i]);
return 0;
}
extern "C" int hex9_to_woct_many(const double *lon, const double *lat, size_t n,
double *xyz) {
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static)
for (ptrdiff_t i = 0; i < N; ++i) {
double cx, cy; int oid;
h9grid::lonlatdeg_to_boct_oid(lon[i], lat[i], &cx, &cy, &oid);
h9grid::boct_to_woct(cx, cy, oid, xyz + (size_t)i * 3);
}
return 0;
}
extern "C" int hex9_from_woct_many(const double *xyz, size_t n,
double *lon, double *lat) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i) {
double cx, cy; int oid;
h9grid::woct_to_boct(xyz + (size_t)i * 3, &cx, &cy, &oid);
rc |= h9grid::boct_oid_to_lonlat(cx, cy, oid, &lon[i], &lat[i]) ? 0 : 1;
}
return rc;
}
extern "C" int hex9_ring_npoints(int densify) {
if (densify < 0 || densify > 9) return -1;
int n = 1;
for (int i = 0; i < densify; ++i) n *= 3;
return 6 * n + 1;
}
/* ── Face-coordinate addressing (bring your own ⟨polyhedron⟩) ────────────────
* The ⟨face⟩⟨cell⟩ layers without AKW: mint and read addresses directly at
* the (cx, cy, oid) chart. Descent is projection-blind, so a caller with her
* own sphere→octahedron map gets the full cell machinery — and the resulting
* addresses are NON-CANONICAL: same 128-bit space, different meaning. The
* projection identity is dataset metadata; never mix projections within one
* dataset (see docs/projection.md). Always the grid-canonical containment
* descent — hex9_set_encoder does not apply here. */
extern "C" int hex9_encode_boct(double cx, double cy, int oid,
uint8_t out_uuid[16]) {
if (!out_uuid || oid < 0 || oid > 7 ||
!std::isfinite(cx) || !std::isfinite(cy)) return 1;
encode_from_boct(cx, cy, oid, out_uuid);
return 0;
}
extern "C" int hex9_encode_boct_many(const double *cx, const double *cy,
const int *oid, size_t n,
uint8_t *out_uuid) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= hex9_encode_boct(cx[i], cy[i], oid[i], out_uuid + (size_t)i * 16);
return rc;
}
/* Cell centroid at the uuid's bin layer, in face coordinates — the same
* lattice centroid hex9_decode unprojects, emitted before the unprojection
* (projection-free; the chart is the returned oid's). */
extern "C" int hex9_decode_boct(const uint8_t uuid[16],
double *cx, double *cy, int *oid) {
if (!uuid || !cx || !cy || !oid) return 1;
if (h9e4h_is_marked(uuid)) return 1; /* E4H input: hex9_e4h_decode */
uint8_t nib[32];
h9a_unpack(uuid, nib);
#if H9_HAS_HTERM
/* Legacy layout: only bin uuids carry the identity path; full L29
* addresses would need the beam walk, which is lon/lat-coupled. */
if (nib[H9_NIB_TAIL - 1] != 0x0Fu) return 1;
#endif
int bl = H9_NIB_BODYTOP;
while (bl >= 0 && nib[bl] == 0x0Fu) bl--;
h9kring::H9CellId id;
if (bl >= 0 && h9kring::identity_from_uuid(uuid, bl, &id) &&
h9cell::identity_centroid_boct(id, bl, cx, cy, oid))
return 0;
return 1;
}
extern "C" int hex9_decode_boct_many(const uint8_t *uuid, size_t n,
double *cx, double *cy, int *oid) {
int rc = 0;
const ptrdiff_t N = (ptrdiff_t)n;
#pragma omp parallel for schedule(static) reduction(|:rc)
for (ptrdiff_t i = 0; i < N; ++i)
rc |= hex9_decode_boct(uuid + (size_t)i * 16, &cx[i], &cy[i], &oid[i]);
return rc;
}
/* Cell boundary ring in face coordinates: (cx, cy, oid) per vertex, each in
* the chart of ITS oid (per-vertex frames, same seam-reflection choice the
* lon/lat ring uses before unprojection). Closed ring — hex9_ring_npoints
* points, last repeats first. Returns point count, or -1 on error. */
extern "C" int hex9_cell_ring_boct(const uint8_t uuid[16], int layer,
int densify,
double *out_cx, double *out_cy,
int *out_oid, int max_points) {
if (!out_cx || !out_cy || !out_oid) return -1;
if (h9e4h_is_marked(uuid)) return -1; /* E4H input: no h9 cell ring */
if (layer < 0 || layer > H9_LMAX) return -1;
if (densify < 0 || densify > 9 || layer + densify > H9_LMAX) return -1;
const int n_ring = hex9_ring_npoints(densify);
if (max_points < n_ring) return -1;
h9kring::H9CellId id;
if (!h9kring::identity_from_uuid(uuid, layer, &id)) return -1;
return h9cell::identity_ring_boct(id, layer, densify, out_cx, out_cy, out_oid);
}
/* ── Labels ─────────────────────────────────────────────────────────────────
* h9_write_label ported from the PostGIS glue (lwgeom_hex9.cpp): body nibbles
* 0..layer mapped to digits 0-9,a,b. Writes layer+1 chars (no NUL); the public
* wrappers NUL-terminate. */
static int write_label(const uint8_t nibbles[32], int layer, char *buf) {
static const char H9D[] = "0123456789ab";
for (int i = 0; i <= layer; ++i) buf[i] = H9D[nibbles[i] & 0x0Fu];
return layer + 1;
}
/* Labels name the CANONICAL bin (== hex9_bin == h9_grid), spelled for humans.
* They are cosmetic, not load-bearing: the body shows the full-hex digit and
* drops the half-hex/mode, so canonical labels are NOT cross-layer
* prefix-nested at split-hex (6/7/8) — common-prefix locality lives in the
* full UUID (the load-bearer) and is a downstream concern. The full UUID is
* decoded to its canonical bin first so label(full) == label(h9_bin(full)). */
extern "C" int hex9_label(const uint8_t uuid[16], int layer, char *buf, size_t buflen) {
if (layer < 0 || layer > H9_LMAX || !buf) return -1;
if (h9_foreign_uuid(uuid)) return -1; /* curve: hex9_curve_label; e4h: hex9_e4h_label */
uint8_t cb[16], nibbles[32];
canonical_bin(uuid, layer, cb);
h9a_unpack(cb, nibbles);
int len = write_label(nibbles, layer, buf);
if (len < 0 || (size_t)len + 1 > buflen) return -1;
buf[len] = '\0';
return len;
}
extern "C" int hex9_label_key(const uint8_t uuid[16], int layer, char *buf, size_t buflen) {
if (layer < 0 || layer > H9_LMAX || !buf) return -1;
if (h9_foreign_uuid(uuid)) return -1; /* curve: hex9_curve_label; e4h: hex9_e4h_label */
/* Canonical bin: it is a bin (nibble[30]==0xF), so nibble[31] already holds
* the layer's key tail (c2<<1 | oct_mode) — no backward walk, and no c_mo
* display bit (the canonical mode-0 home is unambiguous, so GIS exports no
* longer need it to pick a half-hex template). parse(label_key) rebuilds
* this bin exactly, so parse(label_key(u,L)) == hex9_bin(u,L). */
uint8_t cb[16], nibbles[32];
canonical_bin(uuid, layer, cb);
h9a_unpack(cb, nibbles);
int len = write_label(nibbles, layer, buf);
if (len < 0) return -1;
const uint8_t key_tail = nibbles[31];
static const char H9D[] = "0123456789abcdef";
if ((size_t)len + 3 > buflen) return -1; /* '.' + nibble + NUL */
buf[len++] = '.';
buf[len++] = H9D[key_tail & 0x0Fu];
buf[len] = '\0';
return len;
}
/* ── Label parsing / mesh prefix ops ────────────────────────────────────────
* See hex9_c.h. Bare labels are recovered by candidate-tail search verified
* against the canonical re-encode (exact for canonical labels — the only
* kind hex9_label emits for grid/k-ring/containment-encoder UUIDs); keyed
* labels (".<k>") decode directly and are flavour-blind. */
/* Parse the body chars into nibbles; returns layer or -1. */
static int label_body_to_nibbles(const char *label, uint8_t nibbles[32],
const char **tail_out) {
static const char H9D[] = "0123456789ab";
int len = 0;
while (label[len] && label[len] != '.') len++;
if (len < 1 || len > H9_NIB_BODYTOP + 1) return -1; /* body L0..L{BODYTOP} */
for (int i = 0; i < len; i++) {
const char *p = (const char *)std::memchr(H9D, label[i], 12);
if (!p) return -1;
const int d = (int)(p - H9D);
if (i > 0 && d > 8) return -1; /* layers 1+ use digits 0..8 */
nibbles[i] = (uint8_t)d;
}
for (int i = len; i <= H9_NIB_TAIL - 1; i++) nibbles[i] = 0x0Fu;
*tail_out = (label[len] == '.') ? label + len + 1 : nullptr;
return len - 1;
}
extern "C" int hex9_parse_label(const char *label, uint8_t out_uuid[16]) {
if (!label || !out_uuid) return -1;
uint8_t nib[32];
const char *tail;
const int layer = label_body_to_nibbles(label, nib, &tail);
if (layer < 0) return -1;
if (tail) { /* keyed: ".<hex nibble>" */
/* A keyed label IS the bin in text form: parse is the exact
* SYNTACTIC inverse of hex9_label_key — rebuild body + key nibble,
* NO resolution (Python h9_from_label semantics). This makes
* parse(label_key(u, L)) == hex9_bin(u, L) an identity. (The former
* canonicalising parse routed through the bin resolution machinery,
* which is approximate at meta-bearing cells — the doctrine
* fossil.)
*
* The key carries one bit MORE than the bin tail: key =
* (c_mo<<3)|(c2<<1)|r_mo (c_mo lets GIS exports pick the hexagon
* template without resolution), while bin uuids store
* (c2<<1)|r_mo. Strip bit 3 here or the identity silently fails
* for every c_mo=1 cell — first seen 2026-06-12 when the F6 field
* shift landed Westminster L8 on key '.b' (bin tail 3). */
static const char H9D[] = "0123456789abcdef";
const char *p = (const char *)std::memchr(H9D, tail[0], 16);
if (!p || tail[1]) return -1;
nib[31] = (uint8_t)((p - H9D) & 0x7);
h9a_pack(nib, out_uuid);
return layer;
}
/* bare: search the 6 possible tails, require the canonical re-encode to
* reproduce the body (rejects corrupt and non-canonical labels). */
int found = 0;
uint8_t result[16];
for (int r_mo = 0; r_mo < 2; r_mo++) {
for (int c2 = 0; c2 < 3; c2++) {
nib[31] = (uint8_t)((c2 << 1) | r_mo);
uint8_t bin[16];
h9a_pack(nib, bin);
h9kring::H9CellId id;
if (!h9kring::identity_from_uuid(bin, layer, &id)) continue;
uint8_t canon[16], cnib[32];
h9kring::identity_to_uuid(id, layer, canon);
h9a_unpack(canon, cnib);
if (std::memcmp(cnib, nib, (size_t)layer + 1) != 0) continue;
if (found && std::memcmp(result, canon, 16) != 0) return -1; /* ambiguous */
std::memcpy(result, canon, 16);
found = 1;
}
}
if (!found) return -1;
std::memcpy(out_uuid, result, 16);
return layer;
}
extern "C" int hex9_label_centroid(const char *label, double *lon, double *lat) {
if (!lon || !lat) return 1;
uint8_t bin[16];
const int layer = hex9_parse_label(label, bin);
if (layer < 0) return 1;
h9kring::H9CellId id;
if (!h9kring::identity_from_uuid(bin, layer, &id)) return 1;
return h9cell::identity_centroid(id, layer, lon, lat) ? 0 : 1;
}
extern "C" int hex9_common_ancestor(const uint8_t *uuids, size_t n, int layer,
char *buf, size_t buflen, uint8_t *out_uuid) {
if (!uuids || n == 0 || layer < 0 || layer > H9_LMAX || !buf) return -1;
for (size_t i = 0; i < n; ++i)
if (h9e4h_is_marked(uuids + i * 16)) return -1;
uint8_t first[16], fnib[32];
h9kring::H9CellId id;
if (!h9kring::identity_from_uuid(uuids, layer, &id)) return -1;
h9kring::identity_to_uuid(id, layer, first);
h9a_unpack(first, fnib);
int shared = layer + 1; /* nibbles 0..layer */
for (size_t i = 1; i < n && shared > 0; i++) {
uint8_t canon[16], cnib[32];
if (!h9kring::identity_from_uuid(uuids + i * 16, layer, &id)) return -1;
h9kring::identity_to_uuid(id, layer, canon);
h9a_unpack(canon, cnib);
while (shared > 0 && std::memcmp(cnib, fnib, (size_t)shared) != 0) shared--;
}
if (shared == 0) return -1; /* cells span L0 hexes */
const int anc_layer = shared - 1;
/* The ancestor's label IS the shared nibble prefix (write_label is a
* pure nibble->char map); its canonical bin is recovered by the label
* parser's verified tail search. Do NOT label via hex9_label(first,
* anc_layer): that re-coarsens the bin through the identity/context
* walk — the F3 fossil path, unguaranteed for bins below their own
* layer (it mislabelled GB's L1 ancestor '43' as '65' on the L30
* layout). The prefix needs no walk at all. */
if (buflen < (size_t)shared + 1) return -1;
write_label(fnib, anc_layer, buf);
buf[shared] = '\0';
uint8_t anc[16];
if (hex9_parse_label(buf, anc) != anc_layer) return -1;
if (out_uuid) std::memcpy(out_uuid, anc, 16);
return anc_layer;
}
/* ── Single-cell geometry ───────────────────────────────────────────────────
* Identity-based (core/h9_cell_geom.h): the UUID resolves to its exact
* lattice identity and renders through the same construction the grid
* enumerator uses, so h9_cell == h9_grid for every encoding flavour. (The
* former cell_unpack float backward-walk followed the centroid-descent
* convention and could render the wrong hexagon for canonical bins.) */
/* ── Cell lattice identity (integer UV) — see hex9_c.h ─────────────────── */
/* Canonical pool key for a vertex. resolve_uv_frame canonicalises only
* STRICTLY out-of-face vertices; a vertex ON a face boundary is in-face for
* every frame that shares the edge, so each cell would otherwise report it
* in its own frame and pooling would miss the match (found by test: the
* L0-descended seam chains). Rule: enumerate the equivalent representations
* across every edge the vertex lies on (seam maps; corners compose) and
* return the lexicographically smallest (oid, ia, ib). Deterministic, frame
* independent, exact integers throughout. */
static void uv_canonical(int64_t s, int64_t *u, int64_t *v, int *oid) {
struct Rep { int64_t u, v; int oid; };
Rep reps[8];
int n = 0;
reps[n++] = { *u, *v, *oid };
for (int i = 0; i < n; ++i) {
const int mo = (int)H9_OID_MO[reps[i].oid];
for (int e = 0; e < 3; ++e) {
int64_t ru, rv;
h9kring::seam_apply(e, mo, s, reps[i].u, reps[i].v, &ru, &rv);
const int nb = H9_OID_NB[reps[i].oid][e];
if (!h9cell::uv_in_face((int)H9_OID_MO[nb], s, ru, rv)) continue;
bool dup = false;
for (int k = 0; k < n; ++k)
if (reps[k].u == ru && reps[k].v == rv && reps[k].oid == nb) {
dup = true;
break;
}
if (!dup && n < 8) reps[n++] = { ru, rv, nb };
}
}
/* only boundary vertices have orbit > 1; interior verts fall through */
int best = 0;
for (int i = 1; i < n; ++i)
if (reps[i].oid < reps[best].oid ||
(reps[i].oid == reps[best].oid &&
(reps[i].u < reps[best].u ||
(reps[i].u == reps[best].u && reps[i].v < reps[best].v))))
best = i;
*u = reps[best].u;
*v = reps[best].v;
*oid = reps[best].oid;
}
extern "C" int hex9_cell_uv(const uint8_t uuid[16], int layer,
int64_t *c_ia, int64_t *c_ib, int *c_oid,
int64_t v_ia[6], int64_t v_ib[6], int v_oid[6],
int *ext) {
if (layer < 0 || layer > H9_LMAX) return 1;
if (h9e4h_is_marked(uuid)) return 1; /* E4H input: no h9 lattice id */