@alvarosilva/hex-address
Version:
Convert GPS coordinates to memorable hex addresses using H3
1,553 lines (1,548 loc) • 53.6 kB
JavaScript
// src/h3-syllable-system.ts
import { latLngToCell, cellToLatLng, cellToParent, cellToChildren, getBaseCellNumber } from "h3-js";
// src/types.ts
var H3SyllableError = class extends Error {
constructor(message) {
super(message);
this.name = "H3SyllableError";
}
};
var ConversionError = class extends H3SyllableError {
constructor(message) {
super(message);
this.name = "ConversionError";
}
};
// src/configs/ascii-elomr.json
var ascii_elomr_default = {
name: "ascii-elomr",
description: "Basic Latin alphabet, 11 consonants, 5 vowels, 8 syllables",
consonants: [
"d",
"f",
"h",
"k",
"l",
"m",
"n",
"p",
"r",
"s",
"t"
],
vowels: [
"a",
"e",
"i",
"o",
"u"
],
address_length: 8,
h3_resolution: 14,
metadata: {
alphabet: "ascii",
base26_identifier: "elomr",
binary_array: [
1,
0,
0,
1,
1,
1,
0,
1,
1,
0,
1,
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
0,
0,
0,
0,
0
],
selected_letters: [
"a",
"d",
"e",
"f",
"h",
"i",
"k",
"l",
"m",
"n",
"o",
"p",
"r",
"s",
"t",
"u"
],
auto_generated: true,
generation_method: "international_standard",
total_syllables: 55,
total_combinations: 83733937890625,
h3_target_space: 82743214887578,
coverage_ratio: 1.0119734651885726,
coverage_multiple: "1.01x"
}
};
// src/config-loader.ts
var BUNDLED_CONFIGS = {
"ascii-elomr": ascii_elomr_default
};
var ConfigLoader = class {
constructor() {
this.configs = /* @__PURE__ */ new Map();
this.loadBundledConfigs();
}
/**
* Load bundled configurations
*/
loadBundledConfigs() {
for (const [name, configData] of Object.entries(BUNDLED_CONFIGS)) {
try {
const config = {
...configData,
h3_resolution: configData.h3_resolution || 14
};
this.configs.set(name, config);
} catch (error) {
console.warn(`Failed to load bundled config ${name}:`, error);
}
}
if (this.configs.size === 0) {
throw new Error("No valid configuration files found");
}
}
/**
* Get a configuration by name
*/
getConfig(configName) {
if (!/^[a-zA-Z0-9_-]+$/.test(configName)) {
throw new Error(`Invalid configuration name format: ${configName}`);
}
const config = this.configs.get(configName);
if (!config) {
const available = Array.from(this.configs.keys()).join(", ");
throw new Error(`Configuration '${configName}' not found. Available: ${available}`);
}
return config;
}
/**
* Get all configurations
*/
getAllConfigs() {
return new Map(this.configs);
}
/**
* List all configuration names
*/
listConfigs() {
return Array.from(this.configs.keys());
}
/**
* Get configuration metadata
*/
getConfigInfo(configName) {
const config = this.getConfig(configName);
return {
name: config.name,
description: config.description,
consonantsCount: config.consonants.length,
vowelsCount: config.vowels.length,
addressLength: config.address_length,
totalCombinations: config.metadata?.total_combinations,
coverageRatio: config.metadata?.coverage_ratio
};
}
};
var globalConfigLoader;
function getConfig(configName) {
if (!globalConfigLoader) {
globalConfigLoader = new ConfigLoader();
}
return globalConfigLoader.getConfig(configName);
}
function getAllConfigs() {
if (!globalConfigLoader) {
globalConfigLoader = new ConfigLoader();
}
return globalConfigLoader.getAllConfigs();
}
function listConfigs() {
if (!globalConfigLoader) {
globalConfigLoader = new ConfigLoader();
}
return globalConfigLoader.listConfigs();
}
// src/h3-syllable-system.ts
var PHONETIC_CONFUSIONS = {
// Voiced/Unvoiced consonant pairs (most common confusions)
"d": ["t"],
"t": ["d"],
"f": ["v", "p"],
// f often confused with v and p
"v": ["f", "b"],
// v often confused with f and b
"s": ["z", "c"],
// s often confused with z and soft c
"z": ["s"],
"p": ["b", "f"],
// p often confused with b and f
"b": ["p", "v"],
// b often confused with p and v
"k": ["c", "g"],
// k often confused with hard c and g
"c": ["k", "s"],
// c can sound like k or s
"g": ["k", "j"],
// g often confused with k and j
"j": ["g", "y"],
// j often confused with g and y
// Liquid consonants (often confused)
"l": ["r", "n"],
// l/r confusion common in many languages
"r": ["l"],
"n": ["m", "l"],
// n often confused with m and l
"m": ["n"],
// Sibilants and fricatives
"h": ["f"],
// h often confused with f
"w": ["v", "u"],
// w often confused with v and u
"y": ["j", "i"],
// y often confused with j and i
// Vowel confusions (very common in speech)
"a": ["e", "o"],
// a often confused with e and o
"e": ["i", "a"],
// e often confused with i and a
"i": ["e", "y"],
// i often confused with e and y
"o": ["u", "a"],
// o often confused with u and a
"u": ["o", "w"]
// u often confused with o and w
};
var H3SyllableSystem = class _H3SyllableSystem {
constructor(configName = "ascii-elomr") {
this.syllableToIndex = /* @__PURE__ */ new Map();
this.indexToSyllable = /* @__PURE__ */ new Map();
this.hamiltonianPath = [];
this.configName = configName;
this.config = getConfig(configName);
this.consonantCount = this.config.consonants.length;
this.vowelCount = this.config.vowels.length;
this.totalSyllables = this.consonantCount * this.vowelCount;
this.initializeSyllableTables();
this.initializeHamiltonianPath();
}
/**
* Initialize syllable lookup tables for fast conversion
*/
initializeSyllableTables() {
this.syllableToIndex.clear();
this.indexToSyllable.clear();
let index = 0;
for (const consonant of this.config.consonants) {
for (const vowel of this.config.vowels) {
const syllable = consonant + vowel;
this.syllableToIndex.set(syllable, index);
this.indexToSyllable.set(index, syllable);
index++;
}
}
}
/**
* Initialize Hamiltonian path for level 0 cells (optimized array-based approach)
*/
initializeHamiltonianPath() {
this.hamiltonianPath = [
1,
2,
3,
8,
0,
4,
12,
9,
5,
10,
14,
13,
7,
22,
11,
6,
17,
39,
16,
42,
41,
23,
18,
37,
15,
38,
21,
40,
20,
25,
34,
19,
35,
33,
43,
47,
44,
36,
24,
69,
45,
31,
27,
26,
29,
48,
46,
57,
65,
32,
66,
56,
67,
30,
55,
54,
50,
68,
28,
70,
52,
63,
59,
49,
58,
61,
64,
75,
51,
93,
74,
92,
53,
91,
72,
62,
60,
87,
71,
86,
89,
77,
107,
73,
94,
76,
109,
82,
90,
96,
88,
97,
84,
121,
78,
85,
108,
95,
106,
100,
83,
80,
81,
98,
110,
99,
101,
79,
119,
120,
111,
105,
113,
103,
114,
112,
104,
102,
118,
116,
115,
117
];
}
/**
* Convert geographic coordinates to syllable address
*/
coordinateToAddress(latitude, longitude) {
try {
this.validateCoordinates(latitude, longitude);
const h3CellId = latLngToCell(latitude, longitude, this.config.h3_resolution);
const hierarchicalArray = this.h3CellIdToHierarchicalArray(h3CellId);
const integerIndex = this.hierarchicalArrayToIntegerIndex(hierarchicalArray);
const syllableAddress = this.integerIndexToSyllableAddress(integerIndex);
return syllableAddress;
} catch (error) {
if (error instanceof ConversionError) {
throw error;
}
throw new ConversionError(`Coordinate conversion failed`);
}
}
/**
* Convert syllable address to geographic coordinates
*/
addressToCoordinate(syllableAddress) {
try {
const integerIndex = this.syllableAddressToIntegerIndex(syllableAddress);
const hierarchicalArray = this.integerIndexToHierarchicalArray(integerIndex);
const h3CellId = this.hierarchicalArrayToH3CellId(hierarchicalArray);
const [latitude, longitude] = cellToLatLng(h3CellId);
return [latitude, longitude];
} catch (error) {
if (error instanceof ConversionError) {
throw error;
}
console.error("Syllable conversion error:", error);
throw new ConversionError(`Syllable conversion failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
isValidAddress(syllableAddress, detailed) {
if (detailed) {
return this.validateAddress(syllableAddress);
}
try {
this.addressToCoordinate(syllableAddress);
return true;
} catch {
return false;
}
}
/**
* Comprehensive validation with detailed error reporting
* @internal
*/
validateAddress(syllableAddress) {
const errors = [];
const validParts = [];
let suggestions = [];
const formatResult = this.validateFormat(syllableAddress);
if (!formatResult.isValid) {
errors.push(...formatResult.errors);
return { isValid: false, errors, validParts, suggestions };
}
const syllableResult = this.validateSyllables(syllableAddress);
errors.push(...syllableResult.errors);
validParts.push(...syllableResult.validParts);
suggestions.push(...syllableResult.suggestions || []);
if (syllableResult.isValid) {
const geoResult = this.validateGeographic(syllableAddress);
errors.push(...geoResult.errors);
if (!geoResult.isValid) {
suggestions.push(...geoResult.suggestions || []);
}
}
return {
isValid: errors.length === 0,
errors,
validParts,
suggestions: suggestions.length > 0 ? suggestions : void 0
};
}
/**
* Validate address format (length, structure)
*/
validateFormat(address) {
const errors = [];
if (!address || address.trim() === "") {
errors.push({
type: "format",
message: "Address cannot be empty",
suggestions: ['Enter a valid syllable address like "dinenunukiwufeme"']
});
return { isValid: false, errors, validParts: [] };
}
const cleanAddress = address.toLowerCase().trim();
if (cleanAddress.length % 2 !== 0) {
errors.push({
type: "format",
message: `Address length must be even (syllables are 2 characters each). Got ${cleanAddress.length} characters`,
received: cleanAddress,
suggestions: ["Each syllable must be exactly 2 characters (consonant + vowel)"]
});
}
const expectedLength = this.config.address_length * 2;
if (cleanAddress.length !== expectedLength) {
errors.push({
type: "length",
message: `Address must be exactly ${expectedLength} characters (${this.config.address_length} syllables). Got ${cleanAddress.length} characters`,
received: cleanAddress,
expected: [`${expectedLength} characters`]
});
}
return { isValid: errors.length === 0, errors, validParts: [] };
}
/**
* Validate individual syllables
*/
validateSyllables(address) {
const errors = [];
const validParts = [];
const suggestions = [];
const cleanAddress = address.toLowerCase().trim();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
for (let i = 0; i < syllables.length; i++) {
const syllable = syllables[i];
if (syllable.length !== 2) {
errors.push({
type: "syllable",
message: `Syllable at position ${i + 1} must be exactly 2 characters. Got "${syllable}" (${syllable.length} characters)`,
position: i,
received: syllable
});
continue;
}
if (!this.syllableToIndex.has(syllable)) {
const [consonant, vowel] = syllable;
const validConsonants = this.config.consonants;
const validVowels = this.config.vowels;
let errorMsg = `Invalid syllable "${syllable}" at position ${i + 1}`;
const syllableSuggestions = [];
if (!validConsonants.includes(consonant)) {
errorMsg += `. Invalid consonant "${consonant}"`;
const similarConsonants = this.getCharacterSuggestions(consonant, "consonant");
if (similarConsonants.length > 0) {
syllableSuggestions.push(...similarConsonants.map((c) => c + vowel));
}
}
if (!validVowels.includes(vowel)) {
errorMsg += `. Invalid vowel "${vowel}"`;
const similarVowels = this.getCharacterSuggestions(vowel, "vowel");
if (similarVowels.length > 0 && validConsonants.includes(consonant)) {
syllableSuggestions.push(...similarVowels.map((v) => consonant + v));
}
}
errors.push({
type: "syllable",
message: errorMsg,
position: i,
received: syllable,
expected: validConsonants.includes(consonant) ? validVowels.map((v) => consonant + v) : validConsonants.map((c) => c + vowel).slice(0, 5),
suggestions: syllableSuggestions.length > 0 ? syllableSuggestions : void 0
});
} else {
validParts.push(syllable);
}
}
if (errors.length > 0) {
const allSyllables = Array.from(this.syllableToIndex.keys()).sort();
suggestions.push(
`Valid syllables: ${allSyllables.slice(0, 10).join(", ")}...`,
`Consonants: ${this.config.consonants.join(", ")}`,
`Vowels: ${this.config.vowels.join(", ")}`
);
}
return {
isValid: errors.length === 0,
errors,
validParts,
suggestions: suggestions.length > 0 ? suggestions : void 0
};
}
/**
* Validate geographic existence
*/
validateGeographic(address) {
const errors = [];
try {
this.addressToCoordinate(address);
return { isValid: true, errors, validParts: [] };
} catch (error) {
const suggestions = [
"This address combination doesn't correspond to a real geographic location",
"Try modifying the last few syllables",
"Use estimateLocationFromPartial() to explore valid addresses in this area"
];
const similarAddresses = this.findSimilarValidAddresses(address);
if (similarAddresses.length > 0) {
suggestions.unshift(`Similar valid addresses: ${similarAddresses.slice(0, 3).join(", ")}`);
}
errors.push({
type: "geographic",
message: `Address "${address}" doesn't correspond to a real geographic location. This is normal - not all syllable combinations map to valid H3 cells.`,
suggestions
});
}
return { isValid: false, errors, validParts: [] };
}
/**
* Find similar valid addresses by modifying the last few syllables
*/
findSimilarValidAddresses(address, maxAttempts = 50) {
const validAddresses = [];
const cleanAddress = address.toLowerCase().trim();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
const allSyllables = Array.from(this.syllableToIndex.keys());
let attempts = 0;
for (let i = Math.max(0, syllables.length - 2); i < syllables.length && attempts < maxAttempts; i++) {
for (const replacement of allSyllables) {
if (attempts >= maxAttempts) break;
const testSyllables = [...syllables];
testSyllables[i] = replacement;
const testAddress = testSyllables.join("");
if (this.isValidAddress(testAddress)) {
validAddresses.push(testAddress);
attempts++;
}
}
}
return validAddresses;
}
/**
* Get valid phonetic substitutions for a character based on current config
*/
getValidPhoneticSubstitutions(char) {
const allSubstitutions = PHONETIC_CONFUSIONS[char] || [];
const validChars = [...this.config.consonants, ...this.config.vowels];
return allSubstitutions.filter((sub) => validChars.includes(sub));
}
/**
* Get phonetic suggestions for an invalid character
*/
getCharacterSuggestions(char, type) {
const suggestions = /* @__PURE__ */ new Set();
const phoneticSubs = this.getValidPhoneticSubstitutions(char);
phoneticSubs.forEach((sub) => suggestions.add(sub));
if (suggestions.size === 0) {
const validChars = type === "consonant" ? this.config.consonants : this.config.vowels;
validChars.slice(0, 3).forEach((c) => suggestions.add(c));
}
return Array.from(suggestions).slice(0, 5);
}
/**
* Calculate distance between two coordinates in kilometers
*/
calculateDistanceKm(coord1, coord2) {
const [lat1, lng1] = coord1;
const [lat2, lng2] = coord2;
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Analyze a syllable address and provide phonetic alternatives
*/
analyzeAddress(syllableAddress) {
const isValid = this.isValidAddress(syllableAddress);
let coordinates;
if (isValid) {
try {
coordinates = this.addressToCoordinate(syllableAddress);
} catch {
}
}
const phoneticAlternatives = [];
if (coordinates) {
const cleanAddress = syllableAddress.replace(/[-|]/g, "");
for (let i = 0; i < cleanAddress.length; i++) {
const char = cleanAddress[i];
const substitutions = this.getValidPhoneticSubstitutions(char);
for (const substitution of substitutions) {
const altChars = cleanAddress.split("");
altChars[i] = substitution;
const altAddress = this.reconstructAddressFormat(altChars.join(""), syllableAddress);
if (this.isValidAddress(altAddress)) {
try {
const altCoordinates = this.addressToCoordinate(altAddress);
const distance = this.calculateDistanceKm(coordinates, altCoordinates);
phoneticAlternatives.push({
address: altAddress,
coordinates: altCoordinates,
distanceKm: Math.round(distance * 100) / 100,
// Round to 2 decimal places
change: {
position: i,
from: char,
to: substitution
}
});
} catch {
}
}
}
}
}
phoneticAlternatives.sort((a, b) => a.distanceKm - b.distanceKm);
return {
isValid,
address: syllableAddress,
coordinates,
phoneticAlternatives
};
}
/**
* Reconstruct address format (with separators) from clean character string
*/
reconstructAddressFormat(cleanChars, originalFormat) {
let result = "";
let cleanIndex = 0;
for (let i = 0; i < originalFormat.length; i++) {
const char = originalFormat[i];
if (char === "-" || char === "|") {
result += char;
} else {
result += cleanChars[cleanIndex];
cleanIndex++;
}
}
return result;
}
/**
* Estimate location and bounds from a partial syllable address
*/
estimateLocationFromPartial(partialAddress, comprehensive = false) {
try {
const parsed = this.parsePartialAddress(partialAddress);
let samplePoints = [];
let bounds;
let center;
let areaKm2;
if (comprehensive) {
const sampleAddresses = this.generateComprehensiveSamples(parsed);
samplePoints = sampleAddresses.map((addr) => this.addressToCoordinate(addr));
bounds = this.calculateBoundsFromPoints(samplePoints);
center = this.calculateCenterFromPoints(samplePoints);
areaKm2 = this.calculateAreaKm2(bounds);
} else {
const addressRange = this.calculateAddressRange(parsed);
const validRange = this.findValidAddressRange(addressRange.minAddress, addressRange.maxAddress, parsed.completeSyllables);
const minCoords = this.addressToCoordinate(validRange.minAddress);
const maxCoords = this.addressToCoordinate(validRange.maxAddress);
samplePoints = [minCoords, maxCoords];
bounds = this.calculateGeographicBounds(minCoords, maxCoords);
center = this.calculateCenter(minCoords, maxCoords);
areaKm2 = this.calculateAreaKm2(bounds);
}
const confidence = this.calculateConfidence(parsed);
const suggestedRefinements = this.getSuggestedRefinements(parsed);
const completenessLevel = parsed.completeSyllables.length + (parsed.partialConsonant ? 0.5 : 0);
return {
centerCoordinate: center,
bounds,
confidence,
estimatedAreaKm2: areaKm2,
completenessLevel,
suggestedRefinements,
samplePoints: comprehensive ? samplePoints : void 0,
comprehensiveMode: comprehensive
};
} catch (error) {
if (error instanceof ConversionError) {
throw error;
}
throw new ConversionError(`Partial address estimation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Test round-trip conversion accuracy
*/
testRoundTrip(latitude, longitude) {
try {
const syllableAddress = this.coordinateToAddress(latitude, longitude);
const [resultLat, resultLon] = this.addressToCoordinate(syllableAddress);
const latDiff = Math.abs(resultLat - latitude);
const lonDiff = Math.abs(resultLon - longitude);
const latRad = latitude * Math.PI / 180;
const metersPerDegreeLat = 111320;
const metersPerDegreeLon = 111320 * Math.cos(latRad);
const distanceErrorM = Math.sqrt(
(latDiff * metersPerDegreeLat) ** 2 + (lonDiff * metersPerDegreeLon) ** 2
);
return {
success: true,
originalCoordinates: [latitude, longitude],
syllableAddress,
resultCoordinates: [resultLat, resultLon],
distanceErrorMeters: distanceErrorM,
precise: distanceErrorM < 1
};
} catch (error) {
throw new ConversionError(`Round-trip test failed: ${error}`);
}
}
/**
* Get system information and statistics
*/
getSystemInfo() {
const totalSyllables = this.totalSyllables;
const addressSpace = totalSyllables ** this.config.address_length;
const h3Target = 122 * 7 ** 15;
return {
h3Resolution: this.config.h3_resolution,
totalH3Cells: h3Target,
consonants: [...this.config.consonants],
vowels: [...this.config.vowels],
totalSyllables,
addressLength: this.config.address_length,
addressSpace,
coveragePercentage: addressSpace / h3Target * 100,
precisionMeters: 0.5
};
}
/**
* Clear internal cache (no-op - cache removed for performance)
*/
clearCache() {
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Get current configuration name
*/
getConfigName() {
return this.configName;
}
/**
* Create H3 system from a list of letters
*/
static fromLetters(_letters) {
throw new Error("Dynamic config generation not implemented. Use existing configurations.");
}
/**
* Create H3 system with language-optimized configuration
*/
static suggestForLanguage(language = "international", _precisionMeters = 0.5) {
let configName;
switch (language) {
case "english":
configName = "ascii-jaxqt";
break;
case "spanish":
configName = "ascii-fqsmnn";
break;
case "japanese":
configName = "ascii-fqwclj";
break;
default:
configName = "ascii-elomr";
}
return new _H3SyllableSystem(configName);
}
/**
* Validate coordinates are within valid ranges
*/
validateCoordinates(latitude, longitude) {
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
throw new Error(`Invalid coordinate values: latitude=${latitude}, longitude=${longitude}`);
}
if (latitude < -90 || latitude > 90) {
throw new Error(`Latitude must be between -90 and 90, got ${latitude}`);
}
if (longitude < -180 || longitude > 180) {
throw new Error(`Longitude must be between -180 and 180, got ${longitude}`);
}
}
/**
* Convert H3 Cell ID to Hierarchical Array
*/
h3CellIdToHierarchicalArray(h3CellId) {
const hierarchicalArray = new Array(16).fill(-1);
let current = h3CellId;
const parentChain = [current];
for (let res = this.config.h3_resolution - 1; res >= 0; res--) {
const parent = cellToParent(current, res);
parentChain.push(parent);
current = parent;
}
const baseCell = getBaseCellNumber(parentChain[parentChain.length - 1]);
hierarchicalArray[0] = baseCell;
for (let res = 1; res <= this.config.h3_resolution; res++) {
const parent = parentChain[parentChain.length - res];
const child = parentChain[parentChain.length - res - 1];
const children = cellToChildren(parent, res);
const childPosition = children.indexOf(child);
if (childPosition === -1) {
throw new Error(`Could not find child position for resolution ${res}`);
}
hierarchicalArray[res] = childPosition;
}
return hierarchicalArray;
}
/**
* Convert Hierarchical Array to H3 Cell ID
*/
hierarchicalArrayToH3CellId(hierarchicalArray) {
const baseCellNumber = hierarchicalArray[0];
const BASE_CELL_H3_INDICES = {
0: "8001fffffffffff",
1: "8003fffffffffff",
2: "8005fffffffffff",
3: "8007fffffffffff",
4: "8009fffffffffff",
5: "800bfffffffffff",
6: "800dfffffffffff",
7: "800ffffffffffff",
8: "8011fffffffffff",
9: "8013fffffffffff",
10: "8015fffffffffff",
11: "8017fffffffffff",
12: "8019fffffffffff",
13: "801bfffffffffff",
14: "801dfffffffffff",
15: "801ffffffffffff",
16: "8021fffffffffff",
17: "8023fffffffffff",
18: "8025fffffffffff",
19: "8027fffffffffff",
20: "8029fffffffffff",
21: "802bfffffffffff",
22: "802dfffffffffff",
23: "802ffffffffffff",
24: "8031fffffffffff",
25: "8033fffffffffff",
26: "8035fffffffffff",
27: "8037fffffffffff",
28: "8039fffffffffff",
29: "803bfffffffffff",
30: "803dfffffffffff",
31: "803ffffffffffff",
32: "8041fffffffffff",
33: "8043fffffffffff",
34: "8045fffffffffff",
35: "8047fffffffffff",
36: "8049fffffffffff",
37: "804bfffffffffff",
38: "804dfffffffffff",
39: "804ffffffffffff",
40: "8051fffffffffff",
41: "8053fffffffffff",
42: "8055fffffffffff",
43: "8057fffffffffff",
44: "8059fffffffffff",
45: "805bfffffffffff",
46: "805dfffffffffff",
47: "805ffffffffffff",
48: "8061fffffffffff",
49: "8063fffffffffff",
50: "8065fffffffffff",
51: "8067fffffffffff",
52: "8069fffffffffff",
53: "806bfffffffffff",
54: "806dfffffffffff",
55: "806ffffffffffff",
56: "8071fffffffffff",
57: "8073fffffffffff",
58: "8075fffffffffff",
59: "8077fffffffffff",
60: "8079fffffffffff",
61: "807bfffffffffff",
62: "807dfffffffffff",
63: "807ffffffffffff",
64: "8081fffffffffff",
65: "8083fffffffffff",
66: "8085fffffffffff",
67: "8087fffffffffff",
68: "8089fffffffffff",
69: "808bfffffffffff",
70: "808dfffffffffff",
71: "808ffffffffffff",
72: "8091fffffffffff",
73: "8093fffffffffff",
74: "8095fffffffffff",
75: "8097fffffffffff",
76: "8099fffffffffff",
77: "809bfffffffffff",
78: "809dfffffffffff",
79: "809ffffffffffff",
80: "80a1fffffffffff",
81: "80a3fffffffffff",
82: "80a5fffffffffff",
83: "80a7fffffffffff",
84: "80a9fffffffffff",
85: "80abfffffffffff",
86: "80adfffffffffff",
87: "80affffffffffff",
88: "80b1fffffffffff",
89: "80b3fffffffffff",
90: "80b5fffffffffff",
91: "80b7fffffffffff",
92: "80b9fffffffffff",
93: "80bbfffffffffff",
94: "80bdfffffffffff",
95: "80bffffffffffff",
96: "80c1fffffffffff",
97: "80c3fffffffffff",
98: "80c5fffffffffff",
99: "80c7fffffffffff",
100: "80c9fffffffffff",
101: "80cbfffffffffff",
102: "80cdfffffffffff",
103: "80cffffffffffff",
104: "80d1fffffffffff",
105: "80d3fffffffffff",
106: "80d5fffffffffff",
107: "80d7fffffffffff",
108: "80d9fffffffffff",
109: "80dbfffffffffff",
110: "80ddfffffffffff",
111: "80dffffffffffff",
112: "80e1fffffffffff",
113: "80e3fffffffffff",
114: "80e5fffffffffff",
115: "80e7fffffffffff",
116: "80e9fffffffffff",
117: "80ebfffffffffff",
118: "80edfffffffffff",
119: "80effffffffffff",
120: "80f1fffffffffff",
121: "80f3fffffffffff"
};
const baseCell = BASE_CELL_H3_INDICES[baseCellNumber];
if (!baseCell) {
throw new Error(`Invalid base cell number: ${baseCellNumber}. Must be 0-121.`);
}
let currentH3 = baseCell;
for (let res = 1; res <= this.config.h3_resolution; res++) {
const childPosition = hierarchicalArray[res];
const children = cellToChildren(currentH3, res);
if (childPosition >= children.length) {
throw new Error(`Child position ${childPosition} out of range for resolution ${res}`);
}
currentH3 = children[childPosition];
}
return currentH3;
}
/**
* Convert Hierarchical Array to Integer Index using mixed-radix encoding
*/
hierarchicalArrayToIntegerIndex(hierarchicalArray) {
let result = 0;
let multiplier = 1;
for (let pos = this.config.h3_resolution; pos >= 1; pos--) {
const childPos = hierarchicalArray[pos];
if (childPos !== -1) {
result += childPos * multiplier;
multiplier *= 7;
} else {
multiplier *= 7;
}
}
const originalBaseCell = hierarchicalArray[0];
const hamiltonianBaseCell = this.hamiltonianPath[originalBaseCell];
result += hamiltonianBaseCell * multiplier;
return result;
}
/**
* Convert Integer Index to Hierarchical Array
*/
integerIndexToHierarchicalArray(integerIndex) {
const hierarchicalArray = new Array(16).fill(-1);
let remaining = integerIndex;
const baseMultiplier = 7 ** this.config.h3_resolution;
const hamiltonianBaseCell = Math.floor(remaining / baseMultiplier);
const originalBaseCell = this.hamiltonianPath.indexOf(hamiltonianBaseCell);
hierarchicalArray[0] = originalBaseCell;
remaining = remaining % baseMultiplier;
for (let pos = this.config.h3_resolution; pos >= 1; pos--) {
const childPos = remaining % 7;
hierarchicalArray[pos] = childPos;
remaining = Math.floor(remaining / 7);
}
return hierarchicalArray;
}
/**
* Convert Integer Index to Syllable Address
* Orders syllables from coarse to fine geography (most significant first)
*/
integerIndexToSyllableAddress(integerIndex) {
const totalSyllables = this.totalSyllables;
const addressSpace = totalSyllables ** this.config.address_length;
if (integerIndex < 0 || integerIndex >= addressSpace) {
throw new Error(`Integer Index ${integerIndex} out of range [0, ${addressSpace})`);
}
const syllables = [];
let remaining = integerIndex;
for (let pos = 0; pos < this.config.address_length; pos++) {
const syllableIdx = remaining % totalSyllables;
const syllable = this.indexToSyllable.get(syllableIdx);
if (!syllable) {
throw new Error(`Invalid syllable index: ${syllableIdx}`);
}
syllables.unshift(syllable);
remaining = Math.floor(remaining / totalSyllables);
}
return this.formatSyllableAddress(syllables);
}
/**
* Format syllable address as concatenated string
*/
formatSyllableAddress(syllables) {
return syllables.join("");
}
/**
* Convert Syllable Address to Integer Index
* Processes syllables from coarse to fine geography (most significant first)
*/
syllableAddressToIntegerIndex(syllableAddress) {
const cleanAddress = syllableAddress.toLowerCase();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
if (syllables.length !== this.config.address_length) {
throw new Error(`Address must have ${this.config.address_length} syllables`);
}
const totalSyllables = this.totalSyllables;
let integerValue = 0;
for (let pos = 0; pos < syllables.length; pos++) {
const syllable = syllables[syllables.length - 1 - pos];
const syllableIndex = this.syllableToIndex.get(syllable);
if (syllableIndex === void 0) {
throw new Error(`Unknown syllable: ${syllable}`);
}
integerValue += syllableIndex * totalSyllables ** pos;
}
return integerValue;
}
/**
* Parse partial address into syllables array
*/
parsePartialAddress(partialAddress) {
if (!partialAddress || partialAddress.trim() === "") {
throw new ConversionError("Partial address cannot be empty");
}
const cleanAddress = partialAddress.toLowerCase().trim();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
if (syllables.length === 0) {
throw new ConversionError("No valid syllables found in partial address");
}
let partialConsonant;
let completeSyllables = syllables;
const lastSyllable = syllables[syllables.length - 1];
if (lastSyllable.length === 1) {
if (!this.config.consonants.includes(lastSyllable)) {
throw new ConversionError(`Invalid partial consonant: ${lastSyllable}. Must be one of: ${this.config.consonants.join(", ")}`);
}
partialConsonant = lastSyllable;
completeSyllables = syllables.slice(0, -1);
if (completeSyllables.length === 0) {
throw new ConversionError(`Partial address must contain at least one complete syllable. '${partialAddress}' only contains a partial consonant.`);
}
}
if (completeSyllables.length + (partialConsonant ? 1 : 0) >= this.config.address_length) {
throw new ConversionError(`Partial address cannot have ${completeSyllables.length + (partialConsonant ? 1 : 0)} or more syllables (max: ${this.config.address_length - 1})`);
}
for (const syllable of completeSyllables) {
if (!this.syllableToIndex.has(syllable)) {
throw new ConversionError(`Invalid syllable: ${syllable}`);
}
}
return { completeSyllables, partialConsonant };
}
/**
* Calculate the range of complete addresses for a partial address
*/
calculateAddressRange(parsed) {
const totalSyllables = parsed.completeSyllables.length + (parsed.partialConsonant ? 1 : 0);
const remainingSyllables = this.config.address_length - totalSyllables;
if (remainingSyllables < 0) {
throw new ConversionError("Partial address is already complete or too long");
}
const { minSyllable, maxSyllable } = this.getMinMaxSyllables();
let minSyllables;
let maxSyllables;
if (parsed.partialConsonant) {
const firstVowel = this.config.vowels[0];
const lastVowel = this.config.vowels[this.config.vowels.length - 1];
const minPartialSyllable = parsed.partialConsonant + firstVowel;
const maxPartialSyllable = parsed.partialConsonant + lastVowel;
minSyllables = [...parsed.completeSyllables, minPartialSyllable];
for (let i = 0; i < remainingSyllables; i++) {
minSyllables.push(minSyllable);
}
maxSyllables = [...parsed.completeSyllables, maxPartialSyllable];
for (let i = 0; i < remainingSyllables; i++) {
maxSyllables.push(maxSyllable);
}
} else {
minSyllables = [...parsed.completeSyllables];
for (let i = 0; i < remainingSyllables; i++) {
minSyllables.push(minSyllable);
}
maxSyllables = [...parsed.completeSyllables];
for (let i = 0; i < remainingSyllables; i++) {
maxSyllables.push(maxSyllable);
}
}
return {
minAddress: this.formatSyllableAddress(minSyllables),
maxAddress: this.formatSyllableAddress(maxSyllables)
};
}
/**
* Get the minimum and maximum syllables for the current config
*/
getMinMaxSyllables() {
const syllables = Array.from(this.syllableToIndex.keys()).sort();
return {
minSyllable: syllables[0],
maxSyllable: syllables[syllables.length - 1]
};
}
/**
* Calculate geographic bounds from min and max coordinates
*/
calculateGeographicBounds(minCoords, maxCoords) {
const [minLat, minLon] = minCoords;
const [maxLat, maxLon] = maxCoords;
return {
north: Math.max(minLat, maxLat),
south: Math.min(minLat, maxLat),
east: Math.max(minLon, maxLon),
west: Math.min(minLon, maxLon)
};
}
/**
* Calculate center point from min and max coordinates
*/
calculateCenter(minCoords, maxCoords) {
const [minLat, minLon] = minCoords;
const [maxLat, maxLon] = maxCoords;
return [
(minLat + maxLat) / 2,
(minLon + maxLon) / 2
];
}
/**
* Calculate area in square kilometers from geographic bounds
*/
calculateAreaKm2(bounds) {
const latDiff = bounds.north - bounds.south;
const lonDiff = bounds.east - bounds.west;
const avgLat = (bounds.north + bounds.south) / 2;
const latKm = latDiff * 111.32;
const lonKm = lonDiff * 111.32 * Math.cos(avgLat * Math.PI / 180);
return latKm * lonKm;
}
/**
* Calculate confidence score based on completeness level
*/
calculateConfidence(parsed) {
const completenessLevel = parsed.completeSyllables.length + (parsed.partialConsonant ? 0.5 : 0);
const maxLevel = this.config.address_length - 1;
const confidence = 0.1 + completenessLevel / maxLevel * 0.85;
return Math.min(0.95, Math.max(0.1, confidence));
}
/**
* Get suggested refinements (next possible syllables or vowels)
*/
getSuggestedRefinements(parsed) {
const totalSyllables = parsed.completeSyllables.length + (parsed.partialConsonant ? 1 : 0);
if (totalSyllables >= this.config.address_length - 1) {
return [];
}
if (parsed.partialConsonant) {
return this.config.vowels.map((vowel) => parsed.partialConsonant + vowel).sort();
} else {
return Array.from(this.syllableToIndex.keys()).sort();
}
}
/**
* Find valid address range with smart fallback when min/max addresses are invalid
*/
findValidAddressRange(minAddress, maxAddress, partialSyllables) {
const minValid = this.isValidAddress(minAddress);
const maxValid = this.isValidAddress(maxAddress);
if (minValid && maxValid) {
return { minAddress, maxAddress };
}
const maxAttempts = 10;
let validMinAddress = minAddress;
let validMaxAddress = maxAddress;
if (!minValid) {
let attempts = 0;
while (!this.isValidAddress(validMinAddress) && attempts < maxAttempts) {
validMinAddress = this.incrementAddress(validMinAddress, partialSyllables);
attempts++;
}
}
if (!maxValid) {
let attempts = 0;
while (!this.isValidAddress(validMaxAddress) && attempts < maxAttempts) {
validMaxAddress = this.decrementAddress(validMaxAddress, partialSyllables);
attempts++;
}
}
if (this.isValidAddress(validMinAddress) && this.isValidAddress(validMaxAddress)) {
return { minAddress: validMinAddress, maxAddress: validMaxAddress };
}
if (partialSyllables.length > 1) {
console.warn(`Address range for '${partialSyllables.join("")}' is unmappable, falling back to shorter prefix`);
const shorterPartial = partialSyllables.slice(0, -1);
const fallbackRange = this.calculateAddressRange({ completeSyllables: shorterPartial });
return this.findValidAddressRange(fallbackRange.minAddress, fallbackRange.maxAddress, shorterPartial);
}
throw new ConversionError(
`The partial address '${partialSyllables.join("")}' maps to an unmappable region of the H3 address space. This occurs when syllable combinations don't correspond to valid geographic locations. Try a different partial address or use a shorter prefix.`
);
}
/**
* Increment address intelligently from left to right with carry-over
*/
incrementAddress(address, partialSyllables) {
const cleanAddress = address.toLowerCase();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
const allSyllables = Array.from(this.syllableToIndex.keys()).sort();
const partialLength = partialSyllables.length;
for (let i = partialLength; i < syllables.length; i++) {
const currentSyllable = syllables[i];
const currentIndex = allSyllables.indexOf(currentSyllable);
if (currentIndex < allSyllables.length - 1) {
syllables[i] = allSyllables[currentIndex + 1];
for (let j = i + 1; j < syllables.length; j++) {
syllables[j] = allSyllables[0];
}
break;
} else {
syllables[i] = allSyllables[0];
}
}
return this.formatSyllableAddress(syllables);
}
/**
* Decrement address intelligently from left to right with borrow
*/
decrementAddress(address, partialSyllables) {
const cleanAddress = address.toLowerCase();
const syllables = [];
for (let i = 0; i < cleanAddress.length; i += 2) {
syllables.push(cleanAddress.substring(i, i + 2));
}
const allSyllables = Array.from(this.syllableToIndex.keys()).sort();
const partialLength = partialSyllables.length;
for (let i = partialLength; i < syllables.length; i++) {
const currentSyllable = syllables[i];
const currentIndex = allSyllables.indexOf(currentSyllable);
if (currentIndex > 0) {
syllables[i] = allSyllables[currentIndex - 1];
for (let j = i + 1; j < syllables.length; j++) {
syllables[j] = allSyllables[allSyllables.length - 1];
}
break;
} else {
syllables[i] = allSyllables[allSyllables.length - 1];
}
}
return this.formatSyllableAddress(syllables);
}
/**
* Generate sample addresses using comprehensive sampling for all possible syllables at the next level
*/
generateComprehensiveSamples(parsed) {
const sampleAddresses = [];
const allSyllables = Array.from(this.syllableToIndex.keys());
const currentCompleteLength = parsed.completeSyllables.length;
const remainingSyllables = this.config.address_length - currentCompleteLength;
if (remainingSyllables <= 0) {
throw new ConversionError("Address is already complete or too long for comprehensive sampling");
}
if (parsed.partialConsonant) {
for (const vowel of this.config.vowels) {
const completedSyllable = parsed.partialConsonant + vowel;
const prefix = [...parsed.completeSyllables, completedSyllable];
this.addComprehensiveSamplesForPrefix(prefix, remainingSyllables - 1, sampleAddresses, allSyllables);
}
} else {
for (const nextSyllable of allSyllables) {
const prefix = [...parsed.completeSyllables, nextSyllable];
this.addComprehensiveSamplesForPrefix(prefix, remainingSyllables - 1, sampleAddresses, allSyllables);
}
}
return sampleAddresses;
}
/**
* Helper method to add sample addresses for a given prefix using comprehensive sampling
*/
addComprehensiveSamplesForPrefix(prefix, remainingSyllables, sampleAddresses, allSyllables) {
if (remainingSyllables === 0) {
sampleAddresses.push(this.formatSyllableAddress(prefix));
return;
}
const sampleStrategies = [
() => allSyllables[0],
// Min syllable
() => allSyllables[Math.floor(allSyllables.length / 4)],
// 25% point
() => allSyllables[Math.floor(allSyllables.length / 2)],
// Middle
() => allSyllables[Math.floor(3 * allSyllables.length / 4)],
// 75% point
() => allSyllables[allSyllables.length - 1]
// Max syllable
];
for (const getNextSyllable of sampleStrategies) {
const completion = [];
for (let i = 0; i < remainingSyllables; i++) {
completion.push(getNextSyllable());
}
sampleAddresses.push(this.formatSyllableAddress([...prefix, ...completion]));
}
}
/**
* Calculate geographic bounds from multiple coordinate points
*/
calculateBoundsFromPoints(points) {
if (points.length === 0) {
throw new ConversionError("Cannot calculate bounds from empty points array");
}
let north = points[0][0];
let south = points[0][0];
let east = points[0][1];
let west = points[0][1];
for (const [lat, lon] of points) {
north = Math.max(north, lat);
south = Math.min(south, lat);
east = Math.max(east, lon);
west = Math.min(west, lon);
}
return { north, south, east, west };
}
/**
* Calculate center coordinate from multiple points
*/
calculateCenterFromPoints(points) {
if (points.length === 0) {
throw new ConversionError("Cannot calculate center from empty points array");
}
const avgLat = points.reduce((sum, [lat]) => sum + lat, 0) / points.length;
const avgLon = points.reduce((sum, [, lon]) => sum + lon, 0) / points.length;
return [avgLat, avgLon];
}
};
// src/index.ts
function coordinateToAddress(latitude, longitude, configName = "ascii-elomr") {
const system = new H3SyllableSystem(configName);
return system.coordinateToAddress(latitude, longitude);
}
function addressToCoordinate(syllableAddress, configName = "ascii-elomr") {
const system = new H3SyllableSystem(configName);
return system.addressToCoordinate(syllableAddress);
}
function isValidAddress(syllableAddress, configName, detailed) {
const system = new H3SyllableSystem(configName || "ascii-elomr");
if (detailed) {
return system.isValidAddress(syllableAddress, detailed);
}
return system.isValidAddress(syllableAddress);
}
function estimateLocationFromPartial(partialAddress, configName = "ascii-elomr", comprehensive = false) {
const system = new H3SyllableSystem(configName);
return system.estimateLocationFromPartial(partialAddress, comprehensive);
}
function analyzeAddress(syllableAddress, configName = "ascii-elomr") {
const system = new H3SyllableSystem(configName);
return system.analyzeAddress(syllableAddress);
}
function getConfigInfo(configName) {
const system = new H3SyllableSystem(configName);
const config = system.getConfig();
return {
name: config.name,
description: config.description,
consonants: config.consonants,
vowels: config.vowels,
totalSyllables: config.consonants.length * config.vowels.length,
addressLength: config.address_length,
h3Resolution: config.h3_resolution,
addressSpace: (config.consonants.length * config.vowels.length) ** config.address_length
};
}
function listAvailableConfigs() {
return listConfigs();
}
function createSystemFromLetters(letters) {
return H3SyllableSystem.fromLetters(letters);
}
function suggestSystemForLanguage(language = "international", precisionMeters = 0.5) {
return H3SyllableSystem.suggestForLanguage(language, precisionMeters);
}
function listAutoGeneratedConfigs() {
return listConfigs().filter((name) => name.startsWith("ascii-"));
}
function findConfigsByLetters(letters) {
const configs = listConfigs();
const result = [];
for (const configName of configs) {
try {
const system = new H3SyllableSystem(configName);
const config = system.getConfig();
const configLetters = [...config.consonants, ...config.vowels].sort();
const inputLetters = letters.sort();
if (JSON.stringify(configLetters) === JSON.stringify(inputLetters)) {
result.push(configName);
}
} catch {
}
}
return result;
}
function calculateDistance(address1, address2, configName = "ascii-elomr") {
const system = new H3SyllableSystem(configName);
const [lat1, lon1] = system.addressToCoordinate(address1);
const [lat2, lon2] = system.addressToCoordinate(address2);
return haversineDistance(lat1, lon1, lat2, lon2);
}
function findNearbyAddresses(centerAddress, radiusKm, configName = "ascii-elomr") {
const system = new H3SyllableSystem(configName);
const [centerLat, centerLon] = system.addressToCoordinate(centerAddress);
const result = [];
const gridSize = radiusKm / 111;
const stepSize = gridSize / 10;
for (let latOffset = -gridSize; latOffset <= gridSize; latOffset += stepSize) {
for (let lonOffset = -gridSize; lonOffset <= gridSize; lonOffset += stepSize) {
const testLat = centerLat + latOffset;
const testLon = centerLon + lonOffset;
try {
const address = system.coordinateToAddress(testLat,