KunYu
h3-jsngeohashjavascriptspatial-indexnpm

h3-js vs ngeohash: Cómo elegir la librería JavaScript de índice espacial adecuada

Tamaño de bundle, benchmarks de codificación, comparación de APIs y problemas en producción de h3-js y ngeohash — con mediciones reales.

KunYu TeamMarch 25, 202610 min de lectura

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.

Conversor GeoHash

Convierte entre GeoHash, H3 y Plus Codes.

Try it now

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.

Conversor GeoHash

Convierte entre GeoHash, H3 y Plus Codes.

Try it now

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.