/** * Official DigiPin Algorithm (Offline, Zero-dependency) * * Source: India Post – Department of Posts, Government of India * Repository: https://github.com/INDIAPOST-gov/digipin * License: Apache 2.0 * * Developed in collaboration with: * – Indian Institute of Technology, Hyderabad * – National Remote Sensing Centre, ISRO * * This is a TypeScript port vendored for offline DigiPin processing * without any external dependencies. * * DigiPin provides a geo-coded addressing system for India, dividing * the country into ~4m × 4m grids with unique 10-character codes. */ /** DigiPin 4×4 grid character mapping */ const DIGIPIN_GRID: string[][] = [ ['F', 'C', '9', '8'], ['J', '3', '2', '7'], ['K', '4', '5', '6'], ['L', 'M', 'P', 'T'], ]; /** Geographic bounds for India */ const BOUNDS = { minLat: 2.5, maxLat: 38.5, minLon: 63.5, maxLon: 99.5, }; /** Characters that appear in the official 4×4 DigiPin grid */ const DIGIPIN_ALPHABET = 'FC98J327K456LMPT'; const VALID_DIGIPIN_BODY = new RegExp(`^[${DIGIPIN_ALPHABET}]{10}$`, 'i'); const VALID_DIGIPIN_PREFIX = new RegExp(`^[${DIGIPIN_ALPHABET}]{1,10}$`, 'i'); /** Common separators: ASCII dash/underscore/dot plus Unicode dash variants */ const DIGIPIN_SEPARATORS = /[\s\-_\u2010-\u2015\u2212.]/g; /** * Normalise user input to a 10-character DigiPin body (no separators). * Accepts dashed, spaced, or compact forms — e.g. `4P3-J68-637F`, `4P3 J68 637F`, `4P3J68637F`. */ export function normalizeDigiPinInput(input: string): string { return input.trim().toUpperCase().replace(DIGIPIN_SEPARATORS, ''); } /** True when normalised input is a non-empty prefix of a DigiPin (still being typed). */ export function isPotentialDigiPinInput(input: string): boolean { const pin = normalizeDigiPinInput(input); return pin.length > 0 && pin.length <= 10 && VALID_DIGIPIN_PREFIX.test(pin); } /** Canonical display format: XXX-XXX-XXXX */ export function formatDigiPin(input: string): string { const pin = normalizeDigiPinInput(input); if (pin.length !== 10) return input.trim().toUpperCase(); return `${pin.slice(0, 3)}-${pin.slice(3, 6)}-${pin.slice(6)}`; } /** True when input is a complete 10-character DigiPin (any common separator style). */ export function isCompleteDigiPinInput(input: string): boolean { const pin = normalizeDigiPinInput(input); return pin.length === 10 && VALID_DIGIPIN_BODY.test(pin); } export interface DigiPinCoordinates { latitude: string; longitude: string; } /** * Convert lat/lng coordinates to a DigiPin code. * @throws if coordinates are outside India's bounds */ export function getDigiPin(lat: number, lon: number): string { if (lat < BOUNDS.minLat || lat > BOUNDS.maxLat) { throw new Error( `Latitude ${lat} out of range. Must be between ${BOUNDS.minLat} and ${BOUNDS.maxLat}` ); } if (lon < BOUNDS.minLon || lon > BOUNDS.maxLon) { throw new Error( `Longitude ${lon} out of range. Must be between ${BOUNDS.minLon} and ${BOUNDS.maxLon}` ); } let minLat = BOUNDS.minLat; let maxLat = BOUNDS.maxLat; let minLon = BOUNDS.minLon; let maxLon = BOUNDS.maxLon; let digiPin = ''; for (let level = 1; level <= 10; level++) { const latDiv = (maxLat - minLat) / 4; const lonDiv = (maxLon - minLon) / 4; let row = 3 - Math.floor((lat - minLat) / latDiv); let col = Math.floor((lon - minLon) / lonDiv); row = Math.max(0, Math.min(row, 3)); col = Math.max(0, Math.min(col, 3)); digiPin += DIGIPIN_GRID[row]![col]!; if (level === 3 || level === 6) digiPin += '-'; maxLat = minLat + latDiv * (4 - row); minLat = minLat + latDiv * (3 - row); minLon = minLon + lonDiv * col; maxLon = minLon + lonDiv; } return digiPin; } /** * Decode a DigiPin code back to center-point coordinates. * @throws if the DigiPin format is invalid */ export function getLatLngFromDigiPin(digiPin: string): DigiPinCoordinates { const pin = normalizeDigiPinInput(digiPin); if (pin.length !== 10) { throw new Error('Invalid DIGIPIN: must be 10 characters (excluding separators)'); } let minLat = BOUNDS.minLat; let maxLat = BOUNDS.maxLat; let minLon = BOUNDS.minLon; let maxLon = BOUNDS.maxLon; for (let i = 0; i < 10; i++) { const char = pin[i]!; let found = false; let ri = -1; let ci = -1; for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (DIGIPIN_GRID[r]![c] === char) { ri = r; ci = c; found = true; break; } } if (found) break; } if (!found) { throw new Error(`Invalid character in DIGIPIN: '${char}'`); } const latDiv = (maxLat - minLat) / 4; const lonDiv = (maxLon - minLon) / 4; const lat1 = maxLat - latDiv * (ri + 1); const lat2 = maxLat - latDiv * ri; const lon1 = minLon + lonDiv * ci; const lon2 = minLon + lonDiv * (ci + 1); minLat = lat1; maxLat = lat2; minLon = lon1; maxLon = lon2; } return { latitude: ((minLat + maxLat) / 2).toFixed(6), longitude: ((minLon + maxLon) / 2).toFixed(6), }; } /** * Validate a DigiPin string (format + character check). */ export function isValidDigiPin(digiPin: string): boolean { if (!digiPin || typeof digiPin !== 'string') return false; if (!isCompleteDigiPinInput(digiPin)) return false; try { getLatLngFromDigiPin(digiPin); return true; } catch { return false; } } /** * Check if a lat/lng point is within India's DigiPin coverage area. */ export function isWithinIndia(lat: number, lon: number): boolean { return ( lat >= BOUNDS.minLat && lat <= BOUNDS.maxLat && lon >= BOUNDS.minLon && lon <= BOUNDS.maxLon ); } export { BOUNDS as DIGIPIN_BOUNDS };