Se você ainda não decidiu entre hexágonos H3 e retângulos Geohash, leia primeiro nossa comparação H3 vs Geohash vs S2. Este artigo é apenas sobre o próximo passo: qual pacote npm instalar.
h3-js e ngeohash são as bibliotecas JavaScript mais baixadas para H3 e Geohash respectivamente. Usamos ambas na ferramenta de conversão GeoHash & H3 do KunYu. O tamanho do bundle difere em 22x, a velocidade de codificação em 4–6x, e as armadilhas em produção são completamente diferentes. Abaixo estão os números medidos e os problemas que só encontramos depois de colocar em produção.
Tamanho do Bundle: 86 KB vs 3.8 KB
Medido compactando com gzip os entry points do ES module diretamente de node_modules:
| Métrica | h3-js v4.4.0 | ngeohash v0.6.3 |
|---|---|---|
| ES module (gzipped) | 86 KB | 3.8 KB |
| Tamanho total do pacote | 6.5 MB | 52 KB |
| Dependências de runtime | 0 | 0 |
| Implementação | C → Emscripten | Pure JavaScript |
A diferença se resume à implementação. h3-js é a biblioteca C do Uber compilada via Emscripten — projeção icosaédrica, travessia de grade hexagonal, clipping de polígonos, tudo embutido num único arquivo JS, sem possibilidade de tree-shaking. Importar apenas { latLngToCell } ainda envia os 464 KB completos. ngeohash são 6 arquivos JS escritos à mão, e a biblioteca inteira é menor que as definições de tipo do h3-js.
86 KB gzipped é aproximadamente o tamanho do React DOM. Não faz diferença no servidor, mas para um projeto frontend que precisa de apenas uma chamada de encode, é outro React DOM que você está enviando para uma única função.
Velocidade de Codificação
Node.js v24, 100.000 iterações por operação com 1.000 iterações de aquecimento:
| Operação | h3-js | ngeohash | Diferença |
|---|---|---|---|
| Encode (lat/lng → cell) | 850K ops/sec | 4.86M ops/sec | ngeohash 5.7x |
| Decode (cell → lat/lng) | 1.73M ops/sec | 7.58M ops/sec | ngeohash 4.4x |
| Vizinhos (células adjacentes) | 614K ops/sec | 465K ops/sec | h3-js 1.3x |
| Boundary / BBox | 550K ops/sec | 9.29M ops/sec | ngeohash 17x |
O script de benchmark (salve como .mjs e execute com 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 simples, ngeohash domina — operações puras de bit-interleaving em JS estão exatamente no ponto forte do JIT do V8, sem overhead de FFI. h3-js paga custo de marshaling em cada chamada: os argumentos vão para o heap do Emscripten, a função C executa, os resultados são copiados de volta para o JavaScript.
Mas h3-js se destaca na busca de vizinhos. gridDisk percorre a grade icosaédrica num loop C compacto, e a eficiência do algoritmo compilado supera o custo de serialização.
A linha de boundary não é uma comparação justa: cellToBoundary calcula 6 coordenadas de vértice para um hexágono, decode_bbox apenas retorna 4 números. Mas essas são as operações que você realmente chamaria em produção.
A menos que você esteja processando milhões de pontos por requisição, a velocidade de codificação não vai decidir qual biblioteca usar. h3-js codifica uma coordenada em 1,2 microssegundos — rápido o suficiente.
Comparação de API
Encode e 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);
Vizinhos
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);
Boundary da Célula
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 retorna [minLat, minLon, maxLat, maxLon] — sul, oeste, norte, leste. Não é a ordem [west, south, east, north] usada pelo GeoJSON, nem a ordem que o LatLngBounds do Leaflet espera. Erre isso e você terá uma bounding box do outro lado do planeta, sem nenhum erro visível, apenas resultados completamente errados. Sempre desestruture explicitamente:
const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);
h3-js já vem com suas próprias definições de tipo TypeScript; ngeohash requer a instalação separada de @types/ngeohash.
Armadilhas em Produção
IDs de Células H3 Silenciosamente Truncados pela Serialização JSON
Essa foi a que mais nos pegou. IDs de células H3 são inteiros de 64 bits, excedendo o limite de inteiros seguros do JavaScript (2^53 − 1), então h3-js os retorna como strings hexadecimais: "8928308280fffff".
O problema aparece na camada de API. Se qualquer parte do seu backend escreve IDs H3 como números JSON em vez de strings, JSON.parse trunca silenciosamente o valor — você obtém um número válido mas completamente diferente, sem erro, e as consultas retornam resultados vazios. Passamos uma sessão inteira de debugging com dados que simplesmente não batiam entre serviços antes de rastrear o problema até a serialização.
A correção é simples mas precisa ser completa: colunas de string no banco de dados, campos de string no schema da sua API, tipos string nas interfaces TypeScript. Sem exceções. Não assuma "o valor desse ID não é tão grande."
Codificação Inteira do ngeohash Limitada a 52 Bits
encode_int e decode_int usam inteiros em vez de strings Base32, mas são limitados a 52 bits (o limite de inteiro seguro do float64 IEEE 754). Passe bitDepth acima de 52 e a precisão é silenciosamente perdida — nenhum erro é lançado.
O Redis também usa internamente inteiros Geohash de 52 bits, então isso se alinha com o backend mais comum. Mas se você está portando lógica de uma biblioteca Python ou Java que usa inteiros Geohash de 64 bits, os valores não vão bater. É difícil de debugar porque os resultados "parecem próximos", com diferença apenas nos últimos bits.
ngeohash e Outras Bibliotecas Geohash Nem Sempre Concordam
Diferentes implementações de Geohash podem produzir resultados diferentes para a mesma coordenada. A causa é arredondamento de ponto flutuante: quando uma coordenada cai exatamente na fronteira entre duas células, diferentes bibliotecas podem arredondar para vizinhos diferentes.
Encontramos isso ao construir a ferramenta Geohash do KunYu — a saída do ngeohash no frontend e a biblioteca Python geohash no backend ocasionalmente discordavam. É raro, mas real. Para coordenadas GPS (~3 metros de precisão) codificadas em células abrangendo centenas de metros, o impacto prático é mínimo. Mas se você precisa que Node.js e Python/Redis concordem exatamente, escreva testes de integração usando coordenadas em fronteiras de células conhecidas. O comando GEOHASH do Redis é a implementação de referência mais segura para consistência entre sistemas.
h3-js v3 → v4: Todas as Funções Foram Renomeadas
Se o seu projeto ainda está na v3, atualizar para a v4 significa que quase todos os nomes de função mudam:
| v3 | v4 |
|---|---|
geoToH3 |
latLngToCell |
h3ToGeo |
cellToLatLng |
kRing |
gridDisk |
hexRing |
gridRingUnsafe |
compact |
compactCells |
A v4 também mudou o tratamento de erros — entrada inválida lança exceção em vez de retornar null. Existe uma camada de compatibilidade em h3-js/legacy, mas o comportamento de erros segue a semântica da v4. Uma migração única com busca e substituição é mais limpo do que depender do wrapper legado a longo prazo. Tabela completa de renomeações no guia de migração v3→v4.
Qual Usar
| Cenário | Escolha | Motivo em uma linha |
|---|---|---|
| Redis GEO / Elasticsearch | ngeohash | Redis usa Geohash internamente — comportamento compatível |
| Busca por proximidade no frontend | ngeohash | 3.8 KB vs 86 KB |
| Heatmaps hexagonais / Deck.gl | h3-js | Deck.gl suporta nativamente H3HexagonLayer |
| Agregação global (área de célula consistente) | h3-js | A área das células H3 varia <3% entre latitudes |
| IDs legíveis em logs | ngeohash | "9q8yyk" ganha de "8928308280fffff" |
| Cobertura de polígonos / multi-resolução | h3-js | polygonToCells, compactCells, hierarquia pai/filho |
| Processamento em lote no servidor | Qualquer um | Ambos rápidos o suficiente — escolha pelo sistema de índice, não pela biblioteca |
O S2 no ecossistema JavaScript está em situação complicada: s2-geometry não é atualizado há 8 anos, nodes2ts é mais completo mas tem uma comunidade pequena. Se o seu projeto precisa de S2, a abordagem prática é um microserviço em Python/Go rodando a biblioteca oficial, chamado a partir da sua aplicação Node.js.
Para experimentar a codificação Geohash e H3 no navegador, use a ferramenta de conversão GeoHash & H3 do KunYu — cole coordenadas, veja o ID da célula e visualize os limites no mapa.
FAQ
O h3-js usa WebAssembly?
Não. Apesar de ser compilado a partir de C, h3-js usa Emscripten para produzir JavaScript puro em vez de .wasm. A vantagem: nenhum arquivo wasm separado para carregar, funciona em todos os navegadores modernos. A desvantagem: o tamanho do bundle (86 KB gzipped) e o overhead de chamada são ambos maiores do que wasm nativo seria. A comunidade discutiu um build wasm, mas não há plano oficial.
O ngeohash tem exports ESM?
Não tem exports ESM nativos — o pacote é apenas CommonJS. Bundlers de frontend (Vite, webpack) fazem a conversão CJS → ESM automaticamente, então import ngeohash from "ngeohash" funciona normalmente em projetos com bundle. Mas se você está rodando um projeto Node.js puramente ESM com "type": "module", vai precisar de createRequire ou de um fork da comunidade. A biblioteca tem mais de 100K downloads semanais no npm mas não recebe uma publicação desde 2020.
Posso usar ambas as bibliotecas no mesmo projeto?
Sim, sem conflitos. Um padrão comum: o backend usa H3 para agregação e análise espacial, o frontend usa ngeohash para verificações leves de geofence, e eles trocam seus respectivos IDs de célula pela API. Os dois sistemas de índice funcionam independentemente — nenhuma conversão entre eles é necessária.