Si vous n'avez pas encore choisi entre les hexagones H3 et les rectangles Geohash, lisez d'abord notre comparaison H3 vs Geohash vs S2. Cet article porte uniquement sur l'étape suivante : quel package npm installer.
h3-js et ngeohash sont les bibliothèques JavaScript les plus téléchargées pour H3 et Geohash respectivement. Nous utilisons les deux dans l'outil convertisseur GeoHash & H3 de KunYu. La taille du bundle diffère d'un facteur 22, la vitesse d'encodage d'un facteur 4 à 6, et les pièges en production sont complètement différents. Voici les chiffres mesurés et les problèmes que nous n'avons découverts qu'après la mise en production.
Taille du bundle : 86 KB vs 3,8 KB
Mesurée en compressant gzip les points d'entrée ES module directement depuis node_modules :
| Métrique | h3-js v4.4.0 | ngeohash v0.6.3 |
|---|---|---|
| ES module (gzippé) | 86 KB | 3,8 KB |
| Taille totale du package | 6,5 MB | 52 KB |
| Dépendances runtime | 0 | 0 |
| Implémentation | C → Emscripten | JavaScript pur |
L'écart vient de l'implémentation. h3-js est la bibliothèque C d'Uber compilée via Emscripten : projection icosaédrique, parcours de grille hexagonale, découpage de polygones, le tout dans un seul fichier JS, sans possibilité de tree-shaking. Importer uniquement { latLngToCell } embarque quand même les 464 KB complets. ngeohash, ce sont 6 fichiers JS écrits à la main. La bibliothèque entière est plus petite que les définitions de types de h3-js.
86 KB gzippé, c'est à peu près la taille de React DOM. Ça n'a pas d'importance côté serveur, mais pour un projet frontend qui n'a besoin que d'un seul appel d'encodage, c'est un React DOM supplémentaire que vous embarquez pour une seule fonction.
Vitesse d'encodage : ngeohash 4 à 6x plus rapide
Node.js v24, 100 000 itérations par opération avec un préchauffage de 1 000 itérations :
| Opération | h3-js | ngeohash | Écart |
|---|---|---|---|
| Encodage (lat/lng → cellule) | 850K ops/sec | 4,86M ops/sec | ngeohash 5,7x |
| Décodage (cellule → lat/lng) | 1,73M ops/sec | 7,58M ops/sec | ngeohash 4,4x |
| Voisins (cellules adjacentes) | 614K ops/sec | 465K ops/sec | h3-js 1,3x |
| Boundary / BBox | 550K ops/sec | 9,29M ops/sec | ngeohash 17x |
Le script de benchmark (enregistrez-le en .mjs et exécutez avec 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));
Pour l'encodage et le décodage simples, ngeohash domine. Les opérations de bit-interleaving en JS pur sont dans la zone de confort du JIT V8, sans surcoût FFI. h3-js paie un coût de marshaling à chaque appel : les arguments sont copiés dans le tas Emscripten, la fonction C s'exécute, puis les résultats sont recopiés vers JavaScript.
Mais h3-js prend l'avantage sur la recherche de voisins. gridDisk parcourt la grille icosaédrique dans une boucle C compacte, et l'efficacité de l'algorithme compilé compense le coût de sérialisation.
La ligne boundary n'est pas une comparaison équitable : cellToBoundary calcule les coordonnées de 6 sommets pour un hexagone, tandis que decode_bbox renvoie simplement 4 nombres. Mais ce sont les opérations réelles que vous appelleriez en production.
À moins de traiter des millions de points par requête, la vitesse d'encodage ne sera pas le critère décisif. h3-js encode une coordonnée en 1,2 microseconde, c'est largement suffisant.
Comparaison des API
Encodage et décodage
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);
Voisins
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);
Contour de cellule
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 renvoie [minLat, minLon, maxLat, maxLon] — sud, ouest, nord, est. Ce n'est pas l'ordre [west, south, east, north] utilisé par GeoJSON, ni l'ordre attendu par LatLngBounds de Leaflet. Si vous vous trompez, vous obtiendrez une bounding box de l'autre côté de la planète, sans aucune erreur. Déstructurez toujours explicitement :
const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);
h3-js fournit ses propres définitions de types TypeScript ; ngeohash nécessite l'installation séparée de @types/ngeohash.
Pièges en production
Les identifiants de cellule H3 sont tronqués silencieusement par la sérialisation JSON
Celui-ci nous a causé le plus de problèmes. Les identifiants de cellule H3 sont des entiers 64 bits, dépassant la plage d'entiers sûrs de JavaScript (2^53 − 1), donc h3-js les renvoie sous forme de chaînes hexadécimales : "8928308280fffff".
Le problème apparaît au niveau de la couche API. Si une partie de votre backend écrit les identifiants H3 comme des nombres JSON au lieu de chaînes, JSON.parse tronque silencieusement la valeur. Vous obtenez un nombre valide mais complètement différent, aucune erreur, et les requêtes renvoient des résultats vides. Nous avons passé une session entière de débogage avec des données qui ne correspondaient tout simplement pas entre les services avant de remonter jusqu'à la sérialisation.
La correction est simple mais doit être exhaustive : colonnes de type string dans la base de données, champs string dans votre schéma API, types string dans les interfaces TypeScript. Aucune exception. Ne partez pas du principe que "la valeur de cet identifiant n'est pas si grande."
L'encodage entier de ngeohash est limité à 52 bits
encode_int et decode_int utilisent des entiers au lieu de chaînes Base32, mais ils sont limités à 52 bits (la limite d'entier sûr du float64 IEEE 754). Si vous passez un bitDepth supérieur à 52, la précision est silencieusement perdue, sans aucune erreur.
Redis utilise aussi des entiers Geohash de 52 bits en interne, donc c'est cohérent avec le backend le plus courant. Mais si vous portez de la logique depuis une bibliothèque Python ou Java qui utilise des entiers Geohash de 64 bits, les valeurs ne correspondront pas. C'est difficile à déboguer car les résultats "semblent proches", seuls les derniers bits diffèrent.
ngeohash et les autres bibliothèques Geohash ne sont pas toujours d'accord
Différentes implémentations de Geohash peuvent produire des résultats différents pour la même coordonnée. La cause est l'arrondi en virgule flottante : quand une coordonnée tombe exactement sur la frontière entre deux cellules, différentes bibliothèques peuvent arrondir vers des voisins différents.
Nous avons rencontré ce problème en développant l'outil Geohash de KunYu : la sortie de ngeohash côté frontend et la bibliothèque Python geohash côté backend n'étaient pas toujours d'accord. C'est rare, mais ça arrive. Pour des coordonnées GPS (~3 mètres de précision) encodées dans des cellules couvrant des centaines de mètres, l'impact pratique est minime. Mais si vous avez besoin que Node.js et Python/Redis soient parfaitement d'accord, écrivez des tests d'intégration avec des coordonnées situées sur des frontières de cellules connues. La commande GEOHASH de Redis est l'implémentation de référence la plus fiable pour la cohérence inter-systèmes.
h3-js v3 → v4 : toutes les fonctions ont été renommées
Si votre projet utilise encore la v3, la mise à niveau vers la v4 implique le renommage de presque toutes les fonctions :
| v3 | v4 |
|---|---|
geoToH3 |
latLngToCell |
h3ToGeo |
cellToLatLng |
kRing |
gridDisk |
hexRing |
gridRingUnsafe |
compact |
compactCells |
La v4 a également changé la gestion des erreurs — les entrées invalides lèvent des exceptions au lieu de renvoyer null. Il existe une couche de compatibilité via h3-js/legacy, mais le comportement des erreurs suit la sémantique v4. Une migration unique par rechercher-remplacer est plus propre que de dépendre du wrapper legacy à long terme. Table de renommage complète dans le guide de migration v3→v4.
Lequel utiliser
| Scénario | Choix | Raison en une ligne |
|---|---|---|
| Redis GEO / Elasticsearch | ngeohash | Redis utilise Geohash en interne — comportement identique |
| Recherche de proximité frontend | ngeohash | 3,8 KB vs 86 KB |
| Heatmaps hexagonales / Deck.gl | h3-js | Deck.gl supporte nativement H3HexagonLayer |
| Agrégation globale (surface de cellule constante) | h3-js | La surface des cellules H3 varie de moins de 3 % selon les latitudes |
| Identifiants lisibles dans les logs | ngeohash | "9q8yyk" est plus lisible que "8928308280fffff" |
| Couverture polygonale / multi-résolution | h3-js | polygonToCells, compactCells, hiérarchie parent/enfant |
| Traitement batch côté serveur | Les deux | Les deux sont assez rapides — choisissez selon le système d'index, pas la bibliothèque |
S2 dans l'écosystème JavaScript est en mauvais état : s2-geometry n'a pas été mis à jour depuis 8 ans, nodes2ts est plus complet mais a une petite communauté. Si votre projet nécessite S2, l'approche pragmatique est un microservice Python/Go utilisant la bibliothèque officielle, appelé depuis votre application Node.js.
Pour essayer l'encodage Geohash et H3 dans le navigateur, utilisez l'outil convertisseur GeoHash & H3 de KunYu. Collez des coordonnées, visualisez l'identifiant de cellule et affichez les contours sur une carte.
FAQ
Est-ce que h3-js utilise WebAssembly ?
Non. Bien qu'il soit compilé à partir de C, h3-js utilise Emscripten pour produire du JavaScript pur plutôt qu'un fichier .wasm. L'avantage : pas de fichier wasm séparé à charger, fonctionne dans tous les navigateurs modernes. L'inconvénient : la taille du bundle (86 KB gzippé) et le surcoût d'appel sont tous deux plus élevés que ce que le wasm natif permettrait. La communauté a discuté d'un build wasm, mais il n'y a pas de plan officiel.
Est-ce que ngeohash a des exports ESM ?
Pas d'exports ESM natifs, le package est uniquement CommonJS. Les bundlers frontend (Vite, webpack) gèrent automatiquement la conversion CJS → ESM, donc import ngeohash from "ngeohash" fonctionne sans problème dans les projets bundlés. Mais si vous utilisez un projet Node.js en ESM pur avec "type": "module", vous aurez besoin de createRequire ou d'un fork communautaire. La bibliothèque compte plus de 100K téléchargements hebdomadaires sur npm mais n'a pas été publiée depuis 2020.
Peut-on utiliser les deux bibliothèques dans le même projet ?
Oui, aucun conflit. Un pattern courant : le backend utilise H3 pour l'agrégation spatiale et l'analytique, le frontend utilise ngeohash pour des vérifications légères de geofencing, et ils échangent leurs identifiants de cellule respectifs via l'API. Les deux systèmes d'index fonctionnent indépendamment, aucune conversion entre eux n'est nécessaire.