Si todavía no decidiste entre los hexágonos H3 y los rectángulos Geohash, lee primero nuestra comparativa H3 vs Geohash vs S2. Este artículo trata solo del siguiente paso: qué paquete npm instalar.
h3-js y ngeohash son las bibliotecas JavaScript más descargadas para H3 y Geohash respectivamente. Usamos ambas en la herramienta de conversión GeoHash & H3 de KunYu. El tamaño de bundle difiere en 22x, la velocidad de codificación en 4–6x, y los problemas en producción son completamente distintos. A continuación están los números medidos y los problemas que solo encontramos después de poner el código en producción.
Tamaño de Bundle: 86 KB vs 3.8 KB
Medido comprimiendo con gzip los entry points del módulo ES directamente desde node_modules:
| Métrica | h3-js v4.4.0 | ngeohash v0.6.3 |
|---|---|---|
| Módulo ES (gzipped) | 86 KB | 3.8 KB |
| Tamaño total del paquete | 6.5 MB | 52 KB |
| Dependencias en runtime | 0 | 0 |
| Implementación | C → Emscripten | JavaScript puro |
La diferencia se reduce a la implementación. h3-js es la biblioteca en C de Uber compilada con Emscripten: proyección icosaédrica, recorrido de grillas hexagonales, recorte de polígonos, todo empaquetado en un solo archivo JS, sin posibilidad de tree-shaking. Importar solo { latLngToCell } igual envía los 464 KB completos. ngeohash son 6 archivos JS escritos a mano — la biblioteca entera es más pequeña que las definiciones de tipos de h3-js.
86 KB gzipped es aproximadamente el tamaño de React DOM. No importa en el servidor, pero para un proyecto frontend que solo necesita una llamada de encode, es otro React DOM que estás enviando por una sola función.
Velocidad de Codificación: ngeohash 4–6x Más Rápido
Node.js v24, 100,000 iteraciones por operación con 1,000 iteraciones de warmup:
| Operación | h3-js | ngeohash | Diferencia |
|---|---|---|---|
| Encode (lat/lng → celda) | 850K ops/sec | 4.86M ops/sec | ngeohash 5.7x |
| Decode (celda → lat/lng) | 1.73M ops/sec | 7.58M ops/sec | ngeohash 4.4x |
| Vecinos (celdas adyacentes) | 614K ops/sec | 465K ops/sec | h3-js 1.3x |
| Boundary / BBox | 550K ops/sec | 9.29M ops/sec | ngeohash 17x |
El script de benchmark (guárdalo como .mjs y ejecútalo 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));
Para encode/decode simple, ngeohash domina — las operaciones de bit-interleaving en JavaScript puro caen justo en el sweet spot del JIT de V8, sin overhead de FFI. h3-js paga un costo de marshaling en cada llamada: los argumentos van al heap de Emscripten, la función C se ejecuta y los resultados se copian de vuelta a JavaScript.
Pero h3-js se adelanta en la búsqueda de vecinos. gridDisk recorre la grilla icosaédrica en un loop C ajustado, y la eficiencia del algoritmo compilado supera el costo de serialización.
La fila de boundary no es una comparación justa: cellToBoundary calcula 6 coordenadas de vértices para un hexágono, decode_bbox solo devuelve 4 números. Pero estas son las operaciones que realmente llamarías en producción.
A menos que estés procesando millones de puntos por request, la velocidad de codificación no va a decidir qué biblioteca usar. h3-js codifica una coordenada en 1.2 microsegundos — suficientemente rápido.
Comparación de API
Encode y Decode
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);
Vecinos
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);
Límite de Celda
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 devuelve [minLat, minLon, maxLat, maxLon] — sur, oeste, norte, este. No es el orden [west, south, east, north] que usa GeoJSON, ni el orden que espera LatLngBounds de Leaflet. Si lo confundes vas a obtener un bounding box en el lado opuesto del planeta, sin error, solo resultados completamente incorrectos. Siempre desestructura explícitamente:
const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);
h3-js incluye sus propias definiciones de tipos TypeScript; ngeohash requiere instalar @types/ngeohash por separado.
Problemas en Producción
Los IDs de Celda H3 se Truncan Silenciosamente en la Serialización JSON
Este fue el que más nos golpeó. Los IDs de celda H3 son enteros de 64 bits, excediendo el rango de enteros seguros de JavaScript (2^53 − 1), por lo que h3-js los devuelve como cadenas hexadecimales: "8928308280fffff".
El problema aparece en la capa de API. Si alguna parte de tu backend escribe los IDs H3 como números JSON en lugar de cadenas, JSON.parse trunca silenciosamente el valor — obtienes un número válido pero completamente diferente, sin error, y las consultas devuelven resultados vacíos. Pasamos una sesión completa de depuración con datos que simplemente no coincidían entre servicios antes de rastrear el problema hasta la serialización.
La solución es simple pero debe ser exhaustiva: columnas de tipo string en la base de datos, campos string en el esquema de tu API, tipos string en las interfaces de TypeScript. Sin excepciones. No asumas "el valor de este ID no es tan grande."
La Codificación Entera de ngeohash Está Limitada a 52 Bits
encode_int y decode_int usan enteros en lugar de cadenas Base32, pero están limitados a 52 bits (el límite de enteros seguros de float64 IEEE 754). Si pasas un bitDepth mayor a 52, la precisión se pierde silenciosamente — sin error.
Redis también usa internamente enteros Geohash de 52 bits, así que esto se alinea con el backend más común. Pero si estás portando lógica desde una biblioteca de Python o Java que usa enteros Geohash de 64 bits, los valores no van a coincidir. Es difícil de depurar porque los resultados "se ven parecidos"; solo los últimos bits difieren.
ngeohash y Otras Bibliotecas Geohash No Siempre Coinciden
Diferentes implementaciones de Geohash pueden producir resultados distintos para la misma coordenada. La causa es el redondeo de punto flotante: cuando una coordenada cae exactamente en el límite entre dos celdas, diferentes bibliotecas pueden redondear hacia vecinos distintos.
Nos topamos con esto mientras construíamos la herramienta Geohash de KunYu — la salida de ngeohash en el frontend y la biblioteca geohash de Python en el backend ocasionalmente no coincidían. Es raro, pero real. Para coordenadas GPS (~3 metros de precisión) codificadas en celdas que abarcan cientos de metros, el impacto práctico es mínimo. Pero si necesitas que Node.js y Python/Redis coincidan exactamente, escribe tests de integración usando coordenadas en límites de celda conocidos. El comando GEOHASH de Redis es la implementación de referencia más segura para consistencia entre sistemas.
h3-js v3 → v4: Todas las Funciones Fueron Renombradas
Si tu proyecto todavía está en v3, actualizar a v4 significa que casi todos los nombres de funciones cambian:
| v3 | v4 |
|---|---|
geoToH3 |
latLngToCell |
h3ToGeo |
cellToLatLng |
kRing |
gridDisk |
hexRing |
gridRingUnsafe |
compact |
compactCells |
v4 también cambió el manejo de errores — la entrada inválida lanza una excepción en vez de devolver null. Existe una capa de compatibilidad en h3-js/legacy, pero el comportamiento de errores sigue la semántica de v4. Una migración única con buscar-y-reemplazar es más limpia que depender del wrapper legacy a largo plazo. Tabla completa de renombramientos en la guía de migración v3→v4.
Cuál Usar
| Escenario | Elige | Razón en una línea |
|---|---|---|
| Redis GEO / Elasticsearch | ngeohash | Redis usa Geohash internamente — comportamiento compatible |
| Búsqueda de proximidad en frontend | ngeohash | 3.8 KB vs 86 KB |
| Mapas de calor hexagonales / Deck.gl | h3-js | Deck.gl soporta nativamente H3HexagonLayer |
| Agregación global (área de celda consistente) | h3-js | El área de celda H3 varía <3% entre latitudes |
| IDs legibles en logs | ngeohash | "9q8yyk" le gana a "8928308280fffff" |
| Cobertura de polígonos / multi-resolución | h3-js | polygonToCells, compactCells, jerarquía padre/hijo |
| Procesamiento batch en servidor | Cualquiera | Ambas son suficientemente rápidas — elige según el sistema de índice, no la biblioteca |
S2 en el ecosistema JavaScript está en mal estado: s2-geometry no se actualiza hace 8 años, nodes2ts es más completo pero tiene una comunidad pequeña. Si tu proyecto necesita S2, el enfoque práctico es un microservicio en Python/Go corriendo la biblioteca oficial, llamado desde tu app Node.js.
Para probar la codificación Geohash y H3 en el navegador, usa la herramienta de conversión GeoHash & H3 de KunYu — pega coordenadas, ve el ID de celda y visualiza los límites en un mapa.
FAQ
¿h3-js usa WebAssembly?
No. A pesar de estar compilada desde C, h3-js usa Emscripten para producir JavaScript plano en vez de .wasm. La ventaja: no hay archivo wasm separado que cargar, funciona en todos los navegadores modernos. La desventaja: el tamaño de bundle (86 KB gzipped) y el overhead por llamada son mayores de lo que sería con wasm nativo. La comunidad ha discutido un build wasm, pero no hay un plan oficial.
¿ngeohash tiene exports ESM?
No tiene exports ESM nativos — el paquete es solo CommonJS. Los bundlers de frontend (Vite, webpack) manejan la conversión CJS → ESM automáticamente, así que import ngeohash from "ngeohash" funciona bien en proyectos con bundler. Pero si estás corriendo un proyecto Node.js puramente ESM con "type": "module", vas a necesitar createRequire o un fork comunitario. La biblioteca tiene más de 100K descargas semanales en npm pero no se publica desde 2020.
¿Puedo usar ambas bibliotecas en el mismo proyecto?
Sí, sin conflictos. Un patrón común: el backend usa H3 para agregación y analítica espacial, el frontend usa ngeohash para chequeos ligeros de geofence, y se intercambian sus respectivos IDs de celda a través de la API. Los dos sistemas de índice funcionan de forma independiente — no se necesita conversión entre ellos.