UNPKG

@alvarosilva/hex-address

Version:

Convert GPS coordinates to memorable hex addresses using H3

1,553 lines (1,548 loc) 53.6 kB
// 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,