KunYu
h3-jsngeohashjavascriptspatial-indexnpm

h3-js vs ngeohash: Scegliere la giusta libreria JavaScript per l'indicizzazione spaziale

Dimensione del bundle, benchmark di codifica, confronto delle API e insidie in produzione di h3-js e ngeohash — con misurazioni reali.

KunYu TeamMarch 25, 202610 min di lettura

Se non hai ancora deciso tra gli esagoni H3 e i rettangoli Geohash, leggi prima il nostro confronto H3 vs Geohash vs S2. Questo articolo riguarda solo il passo successivo: quale pacchetto npm installare.

h3-js e ngeohash sono le librerie JavaScript più scaricate rispettivamente per H3 e Geohash. Le usiamo entrambe nello strumento di conversione GeoHash & H3 di KunYu. La dimensione del bundle differisce di 22 volte, la velocità di codifica di 4–6 volte, e le insidie in produzione sono completamente diverse. Qui trovi i numeri misurati e i problemi che abbiamo scoperto solo dopo il deploy.

Dimensione del bundle: 86 KB vs 3,8 KB

Misurata comprimendo con gzip gli entry point dei moduli ES direttamente da node_modules:

Metrica h3-js v4.4.0 ngeohash v0.6.3
Modulo ES (gzipped) 86 KB 3,8 KB
Dimensione totale pacchetto 6,5 MB 52 KB
Dipendenze runtime 0 0
Implementazione C → Emscripten Pure JavaScript

La differenza sta nell'implementazione. h3-js è la libreria C di Uber compilata tramite Emscripten: proiezione icosaedrica, attraversamento della griglia esagonale, clipping dei poligoni, tutto in un unico file JS senza possibilità di tree-shaking. Importare solo { latLngToCell } spedisce comunque tutti i 464 KB. ngeohash è composto da 6 file JS scritti a mano — l'intera libreria è più piccola delle definizioni di tipo di h3-js.

86 KB gzippati corrispondono più o meno alla dimensione di React DOM. Non conta lato server, ma per un progetto frontend che ha bisogno di una sola chiamata di codifica, stai spedendo un altro React DOM per una singola funzione.

Velocità di codifica: ngeohash 4–6x più veloce

Node.js v24, 100.000 iterazioni per operazione con 1.000 iterazioni di warmup:

Operazione h3-js ngeohash Differenza
Codifica (lat/lng → cella) 850K ops/sec 4,86M ops/sec ngeohash 5,7x
Decodifica (cella → lat/lng) 1,73M ops/sec 7,58M ops/sec ngeohash 4,4x
Vicini (celle adiacenti) 614K ops/sec 465K ops/sec h3-js 1,3x
Confini / BBox 550K ops/sec 9,29M ops/sec ngeohash 17x

Lo script di benchmark (salva come .mjs ed esegui con node):

import { latLngToCell, cellToLatLng, gridDisk, cellToBoundary } from "h3-js";
import ngeohash from "ngeohash";

const ITERATIONS = 100_000;

function bench(name, fn) {
  for (let i = 0; i < 1000; i++) fn(); // warmup
  const start = performance.now();
  for (let i = 0; i < ITERATIONS; i++) fn();
  const ms = performance.now() - start;
  const opsPerSec = Math.round((ITERATIONS / ms) * 1000);
  console.log(`${name}: ${opsPerSec.toLocaleString()} ops/sec`);
}

const h3Cell = latLngToCell(37.7749, -122.4194, 9);
const hash = ngeohash.encode(37.7749, -122.4194, 6);

bench("h3-js  encode",    () => latLngToCell(37.7749, -122.4194, 9));
bench("ngeohash encode",  () => ngeohash.encode(37.7749, -122.4194, 6));
bench("h3-js  decode",    () => cellToLatLng(h3Cell));
bench("ngeohash decode",  () => ngeohash.decode(hash));
bench("h3-js  neighbors", () => gridDisk(h3Cell, 1));
bench("ngeohash neighbors", () => ngeohash.neighbors(hash));
bench("h3-js  boundary",  () => cellToBoundary(h3Cell));
bench("ngeohash bbox",    () => ngeohash.decode_bbox(hash));

Per le semplici operazioni di codifica/decodifica, ngeohash domina — le operazioni di bit-interleaving in puro JS sono esattamente nel punto forte del JIT di V8, senza overhead FFI. h3-js paga un costo di marshaling ad ogni chiamata: gli argomenti vanno nell'heap di Emscripten, la funzione C viene eseguita, i risultati vengono copiati nel contesto JavaScript.

Ma h3-js prende il vantaggio nella ricerca dei vicini. gridDisk attraversa la griglia icosaedrica in un ciclo C compatto, e l'efficienza dell'algoritmo compilato supera il costo della serializzazione.

La riga dei confini non è un confronto equo: cellToBoundary calcola 6 coordinate di vertice per un esagono, decode_bbox restituisce semplicemente 4 numeri. Ma queste sono le operazioni effettive che chiameresti in produzione.

A meno che tu non stia elaborando milioni di punti per richiesta, la velocità di codifica non sarà il fattore decisivo nella scelta della libreria. h3-js codifica una coordinata in 1,2 microsecondi — più che sufficiente.

Confronto delle API

Codifica e decodifica

import { latLngToCell, cellToLatLng } from "h3-js";
import ngeohash from "ngeohash";

// H3: resolution 9 ≈ 0.11 km²
const h3Cell = latLngToCell(37.7749, -122.4194, 9);
// → "8928308280fffff"

// Geohash: precision 6 ≈ 0.74 km²
const hash = ngeohash.encode(37.7749, -122.4194, 6);
// → "9q8yyk"

// Decode
const [lat, lng] = cellToLatLng(h3Cell);
const { latitude, longitude } = ngeohash.decode(hash);

Vicini

import { gridDisk } from "h3-js";

// H3: center + 6 hexagonal neighbors
const h3Neighbors = gridDisk(h3Cell, 1);

// Geohash: 8 neighbors (N, NE, E, SE, S, SW, W, NW)
const ghNeighbors = ngeohash.neighbors(hash);

Confine della cella

import { cellToBoundary } from "h3-js";

// H3: 6 [lat, lng] vertices
const h3Boundary = cellToBoundary(h3Cell);

// Geohash: bounding box
const bbox = ngeohash.decode_bbox(hash);
// → [minLat, minLon, maxLat, maxLon]

decode_bbox restituisce [minLat, minLon, maxLat, maxLon] — sud, ovest, nord, est. Non l'ordine [west, south, east, north] usato da GeoJSON, e non l'ordine che LatLngBounds di Leaflet si aspetta. Se sbagli, ottieni un bounding box dall'altra parte del pianeta. Nessun errore, semplicemente risultati completamente sbagliati. Destruttura sempre in modo esplicito:

const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);

h3-js include le proprie definizioni di tipo TypeScript; ngeohash richiede un'installazione separata di @types/ngeohash.

Insidie in produzione

Gli ID di cella H3 vengono troncati silenziosamente dalla serializzazione JSON

Questa ci ha colpito più di tutte. Gli ID di cella H3 sono interi a 64 bit, superano l'intervallo degli interi sicuri di JavaScript (2^53 − 1), quindi h3-js li restituisce come stringhe esadecimali: "8928308280fffff".

Il problema emerge al livello API. Se qualsiasi parte del tuo backend scrive gli ID H3 come numeri JSON invece che come stringhe, JSON.parse tronca silenziosamente il valore — ottieni un numero valido ma completamente diverso, nessun errore, e le query restituiscono risultati vuoti. Abbiamo passato un'intera sessione di debug con dati che non corrispondevano tra i servizi prima di risalire alla serializzazione.

La soluzione è semplice ma deve essere capillare: colonne stringa nel database, campi stringa nel tuo schema API, tipi stringa nelle interfacce TypeScript. Nessuna eccezione. Non dare per scontato che "il valore di questo ID non è poi così grande."

La codifica intera di ngeohash è limitata a 52 bit

encode_int e decode_int usano interi invece di stringhe Base32, ma sono limitati a 52 bit (il limite degli interi sicuri per i float64 IEEE 754). Passa un bitDepth superiore a 52 e la precisione viene persa silenziosamente — nessun errore lanciato.

Anche Redis usa internamente interi Geohash a 52 bit, quindi questo è allineato con il backend più comune. Ma se stai portando logica da una libreria Python o Java che usa interi Geohash a 64 bit, i valori non corrisponderanno, ed è difficile da debuggare perché i risultati "sembrano vicini" — solo gli ultimi bit differiscono.

ngeohash e altre librerie Geohash non sempre concordano

Implementazioni diverse di Geohash possono produrre risultati diversi per la stessa coordinata. La causa è l'arrotondamento in virgola mobile: quando una coordinata cade esattamente sul confine tra due celle, librerie diverse possono arrotondare verso vicini diversi.

Ci siamo imbattuti in questo mentre costruivamo lo strumento Geohash di KunYu — l'output di ngeohash nel frontend e la libreria Python geohash nel backend occasionalmente non concordavano. È raro, ma reale. Per coordinate GPS (~3 metri di accuratezza) codificate in celle che coprono centinaia di metri, l'impatto pratico è minimo. Ma se hai bisogno che Node.js e Python/Redis concordino esattamente, scrivi test di integrazione usando coordinate su confini di cella noti. Il comando GEOHASH di Redis è l'implementazione di riferimento più affidabile per la consistenza tra sistemi.

h3-js v3 → v4: tutte le funzioni sono state rinominate

Se il tuo progetto è ancora alla v3, l'aggiornamento alla v4 comporta il cambio di nome di quasi tutte le funzioni:

v3 v4
geoToH3 latLngToCell
h3ToGeo cellToLatLng
kRing gridDisk
hexRing gridRingUnsafe
compact compactCells

La v4 ha anche cambiato la gestione degli errori — input non validi lanciano un'eccezione invece di restituire null. Esiste un layer di compatibilità in h3-js/legacy, ma il comportamento degli errori segue la semantica v4. Una migrazione una tantum con find-and-replace è più pulita che dipendere dal wrapper legacy a lungo termine. La tabella completa dei rinominamenti è nella guida alla migrazione v3→v4.

Convertitore GeoHash

Converti tra GeoHash, H3 e Plus Codes.

Try it now

Quale usare

Scenario Scelta Motivazione in breve
Redis GEO / Elasticsearch ngeohash Redis usa Geohash internamente — comportamento corrispondente
Ricerca di prossimità frontend ngeohash 3,8 KB vs 86 KB
Heatmap esagonali / Deck.gl h3-js Deck.gl supporta nativamente H3HexagonLayer
Aggregazione globale (area cella costante) h3-js L'area delle celle H3 varia meno del 3% tra le latitudini
ID leggibili nei log ngeohash "9q8yyk" batte "8928308280fffff"
Copertura poligonale / multi-risoluzione h3-js polygonToCells, compactCells, gerarchia padre/figlio
Elaborazione batch lato server Entrambi Entrambi abbastanza veloci — scegli in base al sistema di indicizzazione, non alla libreria

S2 nell'ecosistema JavaScript è in condizioni precarie: s2-geometry non viene aggiornato da 8 anni, nodes2ts è più completo ma ha una community ridotta. Se il tuo progetto richiede S2, l'approccio pratico è un microservizio Python/Go che esegue la libreria ufficiale, chiamato dalla tua applicazione Node.js.

Per provare la codifica Geohash e H3 nel browser, usa lo strumento di conversione GeoHash & H3 di KunYu — incolla le coordinate, visualizza l'ID della cella e osserva i confini sulla mappa.

Convertitore GeoHash

Converti tra GeoHash, H3 e Plus Codes.

Try it now

FAQ

h3-js usa WebAssembly?

No. Nonostante sia compilato da C, h3-js usa Emscripten per produrre JavaScript puro piuttosto che file .wasm. Il vantaggio: nessun file wasm separato da caricare, funziona in tutti i browser moderni. Lo svantaggio: la dimensione del bundle (86 KB gzippati) e l'overhead delle chiamate sono entrambi maggiori rispetto a quanto offrirebbe un wasm nativo. La community ha discusso una build wasm, ma non c'è un piano ufficiale.

ngeohash ha export ESM?

Nessun export ESM nativo — il pacchetto è solo CommonJS. I bundler frontend (Vite, webpack) gestiscono la conversione CJS → ESM automaticamente, quindi import ngeohash from "ngeohash" funziona senza problemi nei progetti con bundler. Ma se stai eseguendo un progetto Node.js puramente ESM con "type": "module", avrai bisogno di createRequire o di un fork della community. La libreria ha oltre 100K download settimanali su npm ma non viene pubblicata dal 2020.

Posso usare entrambe le librerie nello stesso progetto?

Sì, nessun conflitto. Un pattern comune: il backend usa H3 per aggregazione spaziale e analytics, il frontend usa ngeohash per controlli di geofence leggeri, e si scambiano i rispettivi ID di cella tramite API. I due sistemi di indicizzazione funzionano in modo indipendente — non serve alcuna conversione tra di essi.