Si aún no has decidido 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 librerías JavaScript más descargadas para H3 y Geohash respectivamente. Usamos ambas en la herramienta de conversión GeoHash y H3 de KunYu. El tamaño del bundle difiere en 22x, la velocidad de codificación en 4–6x, y los problemas en producción son completamente distintos. Aquí van las cifras medidas y los problemas que solo descubrimos después de ponerlo en producción.
Tamaño del 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 tiempo de ejecución | 0 | 0 |
| Implementación | C → Emscripten | JavaScript puro |
La diferencia se explica por la implementación. h3-js es la librería C de Uber compilada a través de Emscripten: proyección icosaédrica, recorrido de cuadrícula hexagonal y recorte de polígonos, todo empaquetado en un único archivo JS sin posibilidad de tree-shaking. Importar solo { latLngToCell } sigue enviando los 464 KB completos. ngeohash son 6 archivos JS escritos a mano — la librería 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. En el servidor da igual, pero para un proyecto frontend que solo necesita una llamada de codificación, es otro React DOM que estás enviando por una sola función.
Velocidad de codificación: ngeohash 5,7x más rápido
Node.js v24, 100.000 iteraciones por operación con un calentamiento de 1.000 iteraciones:
| Operación | h3-js | ngeohash | Diferencia |
|---|---|---|---|
| Codificar (lat/lng → celda) | 850K ops/seg | 4,86M ops/seg | ngeohash 5,7x |
| Decodificar (celda → lat/lng) | 1,73M ops/seg | 7,58M ops/seg | ngeohash 4,4x |
| Vecinos (celdas adyacentes) | 614K ops/seg | 465K ops/seg | h3-js 1,3x |
| Boundary / BBox | 550K ops/seg | 9,29M ops/seg | ngeohash 17x |
El script del 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 codificación/decodificación simple, ngeohash domina — las operaciones puras de intercalado de bits en JS caen justo en el punto óptimo del JIT de V8, sin sobrecarga de FFI. h3-js paga un coste de marshaling en cada llamada: los argumentos van al heap de Emscripten, se ejecuta la función C y los resultados se copian de vuelta a JavaScript.
Pero h3-js gana en la búsqueda de vecinos. gridDisk recorre la cuadrícula icosaédrica en un bucle C compacto, y la eficiencia del algoritmo compilado supera el coste de serialización.
La fila de boundary no es una comparación justa: cellToBoundary calcula 6 coordenadas de vértice para un hexágono, decode_bbox simplemente devuelve 4 números. Pero estas son las operaciones reales que llamarías en producción.
A menos que estés procesando millones de puntos por petición, la velocidad de codificación no va a decidir qué librería usar. h3-js codifica una coordenada en 1,2 microsegundos — suficientemente rápido.
Comparación de APIs
Codificar y decodificar
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 te equivocas, obtendrás un bounding box en el lado opuesto del planeta, sin error, simplemente resultados completamente incorrectos. Siempre desestructura de forma explícita:
const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);
h3-js incluye sus propias definiciones de tipos TypeScript; ngeohash requiere instalar por separado @types/ngeohash.
Problemas en producción
Los IDs de celda H3 se truncan silenciosamente en la serialización JSON
Este fue el que más nos afectó. Los IDs de celda H3 son enteros de 64 bits, lo que supera el rango de entero seguro de JavaScript (2^53 − 1), así que h3-js los devuelve como cadenas hexadecimales: "8928308280fffff".
El problema aparece en la capa de API. Si alguna parte de tu backend escribe IDs H3 como números JSON en lugar de cadenas, JSON.parse trunca el valor silenciosamente — obtienes un número válido pero completamente diferente, sin error, y las consultas devuelven resultados vacíos. Pasamos una sesión de depuración completa con datos que simplemente no coincidían entre servicios antes de rastrear el origen hasta la serialización.
La solución es sencilla pero tiene que ser exhaustiva: columnas de tipo string en la base de datos, campos string en tu esquema de API, tipos string en las interfaces TypeScript. Sin excepciones. No asumas que "el valor de este ID no es tan grande".
La codificación de enteros 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 entero seguro de float64 según IEEE 754). Si pasas un bitDepth superior a 52, se pierde precisión silenciosamente — sin lanzar ningún error.
Redis también usa internamente enteros Geohash de 52 bits, así que esto coincide con el backend más común. Pero si estás portando lógica desde una librería Python o Java que usa enteros Geohash de 64 bits, los valores no coincidirán, y es difícil de depurar porque los resultados "se ven similares" — solo difieren los últimos bits.
ngeohash y otras librerías Geohash no siempre coinciden
Diferentes implementaciones de Geohash pueden producir resultados distintos para la misma coordenada. La causa es el redondeo en punto flotante: cuando una coordenada cae exactamente en la frontera entre dos celdas, distintas librerías pueden redondear hacia vecinos diferentes.
Nos topamos con esto al construir la herramienta Geohash de KunYu — la salida de ngeohash en el frontend y la librería Python geohash 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 fronteras de celda conocidas. 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 cambiaron de nombre
Si tu proyecto aún está en v3, actualizar a v4 implica que prácticamente todas las funciones cambian de nombre:
| v3 | v4 |
|---|---|
geoToH3 |
latLngToCell |
h3ToGeo |
cellToLatLng |
kRing |
gridDisk |
hexRing |
gridRingUnsafe |
compact |
compactCells |
v4 también cambió la gestión de errores — las entradas inválidas lanzan una excepción en lugar 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. La tabla completa de renombramientos está 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 coherente |
| 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 por humanos en logs | ngeohash | "9q8yyk" gana a "8928308280fffff" |
| Cobertura poligonal / multirresolución | h3-js | polygonToCells, compactCells, jerarquía padre/hijo |
| Procesamiento por lotes en servidor | Cualquiera | Ambos son suficientemente rápidos — elige según el sistema de índice, no la librería |
S2 en el ecosistema JavaScript está en mal estado: s2-geometry no se ha actualizado en 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 ejecutando la librería oficial, llamado desde tu aplicación Node.js.
Para probar la codificación Geohash y H3 en el navegador, usa la herramienta de conversión GeoHash y H3 de KunYu — pega coordenadas, consulta el ID de celda y visualiza los límites en un mapa.
FAQ
¿Usa h3-js WebAssembly?
No. A pesar de estar compilado desde C, h3-js usa Emscripten para producir JavaScript plano en lugar de un archivo .wasm. La ventaja: no hay un archivo wasm separado que cargar, funciona en todos los navegadores modernos. La desventaja: el tamaño del bundle (86 KB gzipped) y la sobrecarga de cada llamada son mayores de lo que sería con wasm nativo. La comunidad ha debatido un build con wasm, pero no hay un plan oficial.
¿Tiene ngeohash exports ESM?
No tiene exports ESM nativos — el paquete es solo CommonJS. Los bundlers de frontend (Vite, webpack) gestionan la conversión CJS → ESM automáticamente, así que import ngeohash from "ngeohash" funciona bien en proyectos con bundler. Pero si estás ejecutando un proyecto Node.js puramente ESM con "type": "module", necesitarás createRequire o un fork de la comunidad. La librería tiene más de 100K descargas semanales en npm pero no se ha publicado desde 2020.
¿Puedo usar ambas librerías en el mismo proyecto?
Sí, sin conflictos. Un patrón habitual: el backend usa H3 para agregación espacial y analítica, el frontend usa ngeohash para comprobaciones de geofence ligeras, y se intercambian sus respectivos IDs de celda a través de la API. Los dos sistemas de índice funcionan de manera independiente — no es necesario convertir entre ellos.