import { hashCode } from "../utils/hash-code"; import type { MelissaAddressRecord, Source } from "./autocomplete.interface"; import { UnitLogger } from '../utils/unit-logger'; import { getUnitPrefixPatterns } from '../utils/unit-patterns'; import { unitHandlers } from '../utils/unit-handlers'; export interface Address { id?: number; display_address?: string; startsub?: string; ErrorString?: string; displayHash?: string; url_address?: string; latlng?: string; sub?: number; nsubs?: number; hash?: string; street?: string; city?: string; state?: string; zipcode?: string; zipcodePrimary?: string; zipcodeSecondary?: string; validator: "melissa" | "attom"; lat?: string; lng?: string; subUnits?: Address[]; signedAddress?: string; unit?: string; unitNumber?: string; fullUnit?: string; deliveryLine?: string; } export function parseAutocompleteResults(autocompletedata: { ErrorString: string; Results: MelissaAddressRecord[]; }): Address[] { let addresslist = []; if (autocompletedata) { if (autocompletedata.ErrorString != "") { addresslist = []; } else { let hash = ""; const displayhashes: string[] = []; for (var i = 0; i < autocompletedata.Results.length; i++) { var item = autocompletedata.Results[i]; if (!item.Address) continue; if ( !item.Address.Address1 || !item.Address.Locality || !item.Address.PostalCode ) continue; if (!("SubBuilding" in item.Address)) item.Address.SubBuilding = ""; const subs = item.Address.SubBuilding?.split(",") || []; const nsubs = subs?.length; if (nsubs && nsubs > 1) { let address: Address = { validator: "melissa", }; const street = item.Address.Address1; const city = item.Address.Locality; const state = item.Address.AdministrativeArea; const zip = item.Address.PostalCode; address.display_address = street + ", " + city + " " + state + " " + zip; hash = hashCode(address.display_address); address.startsub = hash; address.displayHash = hashCode(address.display_address); address.nsubs = nsubs; if (displayhashes.indexOf(address.displayHash) >= 0) { continue; } addresslist.push(address); } else hash = ""; for (let si = 0; si < subs.length; si++) { let address: Address = { validator: "melissa", signedAddress: item.signedAddress, }; // Store the original unencoded street address const originalStreet = item.Address.Address1; let street = originalStreet; if (subs[si].length > 0) street = street.includes(subs[si]) ? street : street + " " + subs[si]; let city = item.Address.Locality; const state = item.Address.AdministrativeArea; const zip = item.Address.PostalCode; const lat = item.Address.Latitude; const lng = item.Address.Longitude; const zipPrimary = zip.split("-")[0]; const zipSecondary = zip.split("-")[1]; // Create display_address before URL encoding const displayAddress = `${street}, ${city} ${state} ${zip}`; address.display_address = displayAddress; // Now create URL-safe versions for the url_address street = street.replace(/ /g, "+"); street = street.replace("#", "%23"); city = city.replace(/ /g, "+"); address.url_address = street + "-" + city + "-" + state + "-" + zip; address.latlng = "&lat=" + lat + "&lng=" + lng; address.sub = si; address.nsubs = nsubs; address.city = city; address.state = state; address.zipcode = zip; address.zipcodePrimary = zipPrimary; address.zipcodeSecondary = zipSecondary; address.street = item.Address.Address1; address.validator = "melissa"; address.lat = lat; address.lng = lng; if (nsubs > 1) address.hash = hash; address.displayHash = hashCode(address.display_address); if (item?.signedSubUnits) { const subUnit = item?.signedSubUnits?.find(subUnit => subUnit?.unit === subs[si]); address.signedAddress = subUnit?.jwt || item.signedAddress; } if (displayhashes.indexOf(address.displayHash) >= 0) { continue; } // Extract unit information from the full display address instead of just street UnitLogger.source('Melissa', displayAddress); const unitInfo = extractSubUnitInfo(displayAddress); UnitLogger.info('Melissa', unitInfo); address = { ...address, }; addresslist.push(address); } } } } return addresslist; } export function parseAttomAutocompleteResults( addresses: Source[] = [], requestedAddress?: string ) { const parsedAddresses = addresses.map((address) => { let { address: fullAddress, city, state, zipcode, geo_point, signedAddress, } = address; const streetParts = fullAddress.split(city); const mergedStreet = streetParts .slice(0, streetParts.length - 1) .join(city) .trim(); let parsedStreet = mergedStreet.replace(/\s/g, "+"); parsedStreet = parsedStreet.replace(/#/g, "%23"); city = city.replace(/\s/g, "+"); // Extract unit information UnitLogger.source('Attom', fullAddress); const unitInfo = extractSubUnitInfo(fullAddress); UnitLogger.info('Attom', unitInfo); const addressObj: Address = { id: address.id, display_address: fullAddress, street: mergedStreet, url_address: parsedStreet + "-" + city + "-" + state + "-" + zipcode, latlng: "&lat=" + geo_point?.lat + "&lng=" + geo_point?.lon, city, state, lat: geo_point?.lat, lng: geo_point?.lon, zipcode: String(zipcode), subUnits: [], validator: "attom", signedAddress, unit: unitInfo.prefix, unitNumber: unitInfo.number, fullUnit: unitInfo.fullUnit, }; return addressObj; }); const hitsWithSubUnits = groupSubunits(parsedAddresses, requestedAddress); return hitsWithSubUnits; } /** * Groups addresses into buildings and subunits based on address format. * Each address is analyzed to determine if it's a subunit, and if so, it's grouped with its building. * Buildings and their subunits are then returned as a flat list with additional metadata. * * @param {Address[]} addresses - The list of address objects to be grouped. * @param {string} requestedAddress - A specific address requested for grouping; used to determine if special handling is needed. * @returns {Address[]} - An array of addresses with added properties for grouping and hierarchy. */ function groupSubunits( addresses: Address[], requestedAddress?: string ): Address[] { // Map to hold buildings and their subunits for grouping const buildingMap: Map = new Map(); // Regex to identify if an address is a subunit based on common identifiers const requestedAddressIsSubunitRegex = /(?:\b#\b|\bUNIT\b|\bAPT\b|\d+\s+\d+,\s|\d+,\s)/; // Checks if the requestedAddress is a subunit const isRequestedAddressIsSubunit = requestedAddressIsSubunitRegex.test( requestedAddress?.toUpperCase()! ); addresses.forEach((address) => { const regex = /(?:\b#\b|\bUNIT\b|\bAPT\b|\d+\s+\d+,\s|\d+,\s)/; // Determine if the current address is a subunit const isSubunit = regex.test(address.display_address!); // Attempt to isolate the base (building) part of the address let baseAddress = address?.street; // If the address is a subunit, refine the baseAddress to exclude subunit specifics if (isSubunit) { const match = baseAddress?.match( /^(.+?)(?=\s#|\sUNIT|\sAPT|\d+\s+\d+,\s|\d+,\s)/ ); if (match) { baseAddress = match[0].trim(); } } // Construct a unique key for the building based on its address components const buildingKey = `${baseAddress}-${address.city}-${address.state}-${address.zipcode}`; // Generate a hash for the base address for identification const addressHash = hashCode(baseAddress!); // Set the displayHash property for the address address.displayHash = addressHash; // Add the address to the buildingMap, creating a new entry if necessary if (!buildingMap.has(buildingKey)) { buildingMap.set(buildingKey, { ...address, subUnits: isSubunit ? [address] : [], }); } else { if (isSubunit) { buildingMap.get(buildingKey)?.subUnits.push(address); } } // For subunits, associate them with their building's hash if (isSubunit) { address.hash = addressHash; address.displayHash = hashCode(address.display_address!); } }); // Convert the buildingMap into a flat array for output, including subunit details const result: Address[] = []; buildingMap.forEach((value) => { // Handle buildings with subunits, separating subunits if not the requested address format if (value.subUnits.length > 1) { // Deconstruct to separate subUnits for individual processing const { subUnits, ...building } = value; // Add the building with metadata about its subunits result.push({ ...building, nsubs: subUnits.length, // Number of subunits startsub: value.displayHash, // Hash of the building address }); // Sort and include subunits based on display address if requested address is not a subunit if (!isRequestedAddressIsSubunit) { subUnits .sort((a, b) => (a.display_address! > b.display_address! ? 1 : -1)) .forEach((subUnit, index) => { // Assign a sequence and associate with the building's hash result.push({ ...subUnit, hash: value.displayHash, sub: index }); }); } else { // If the requested address is a subunit, include all subunits without sorting value.subUnits.forEach((subUnit, index) => { result.push({ ...subUnit, hash: value.displayHash, sub: index }); }); } } else { // Directly add standalone addresses or those without subunits result.push(value); } }); return result; } export interface SubUnitInfo { prefix?: string; number?: string; fullUnit?: string; } export function extractSubUnitInfo(address: string): SubUnitInfo { UnitLogger.start(address); const result: SubUnitInfo = {}; const unitPrefixes = getUnitPrefixPatterns(); const decodedAddress = address.replace(/\+/g, ' '); const normalizedAddress = decodedAddress.trim().replace(/\s+/g, ' '); UnitLogger.normalized(normalizedAddress); for (const [prefix, pattern] of Object.entries(unitPrefixes)) { UnitLogger.pattern(prefix, pattern.toString()); const match = normalizedAddress.match(pattern); if (match) { UnitLogger.match(prefix, match); const fullMatch = match[0].trim().replace(/,$/, ''); const unitNumber = match[1]; const handler = unitHandlers[prefix] || unitHandlers.DEFAULT; const formattedUnit = handler.formatUnit(unitNumber, fullMatch, prefix); Object.assign(result, formattedUnit); UnitLogger.result(result); break; } } if (!result.prefix && !result.number) { UnitLogger.noMatch(); } return result; }