import type { MothershipAddressCandidate } from "@/modules/address/types"; export type StreetDirection = "N" | "S" | "E" | "W"; const DIRECTION_FULL: Record = { N: "North", S: "South", E: "East", W: "West", }; /** 解析门牌后方位(单字母或 North/East 全称) */ export function parseStreetDirection(street: string): StreetDirection | null { const trimmed = street.trim(); const letter = trimmed.match(/^\d+\s+([NSEW])\b/i); if (letter) { return letter[1]!.toUpperCase() as StreetDirection; } for (const dir of Object.keys(DIRECTION_FULL) as StreetDirection[]) { if (new RegExp(`\\b${DIRECTION_FULL[dir]}\\b`, "i").test(trimmed)) { return dir; } } return null; } /** * 将「600 W 7th St」展开为 MotherShip 更常识别的「600 West 7th Street」。 * 仅处理门牌后单字母方位;无法识别时返回 null。 */ export function expandDirectionalStreet(street: string): string | null { const trimmed = street.trim(); const match = trimmed.match(/^(\d+)\s+([NSEW])\s+(.+)$/i); if (!match) { return null; } const dir = match[2]!.toUpperCase() as StreetDirection; const fullDir = DIRECTION_FULL[dir]; if (!fullDir) { return null; } let rest = match[3]!.trim(); rest = rest.replace(/\bSt\.?$/i, "Street"); return `${match[1]} ${fullDir} ${rest}`.replace(/\s+/g, " ").trim(); } function labelHasDirection( label: string, dir: StreetDirection, ): boolean { const lower = label.toLowerCase(); const full = DIRECTION_FULL[dir].toLowerCase(); if (lower.includes(full)) { return true; } return new RegExp(`\\b\\d+\\s+${dir}\\b`, "i").test(label); } /** 按用户草稿对 MotherShip 候选打分(方位一致优先,相反方位重罚) */ export function scoreMothershipCandidateForDraft( candidate: MothershipAddressCandidate, draft: { street: string; city: string; state: string }, ): number { const label = `${candidate.display_label} ${candidate.street}`.toLowerCase(); let score = 0; const streetNum = draft.street.match(/^\d+/)?.[0]; if (streetNum && label.includes(streetNum)) { score += 12; } const city = draft.city.trim().toLowerCase(); if (city && label.includes(city)) { score += 6; } const state = draft.state.trim().toLowerCase(); if (state && label.includes(state)) { score += 3; } const draftDir = parseStreetDirection(draft.street); if (draftDir) { const opposite: Record = { N: "S", S: "N", E: "W", W: "E", }; if (labelHasDirection(label, draftDir)) { score += 18; } if (labelHasDirection(label, opposite[draftDir])) { score -= 28; } const expanded = expandDirectionalStreet(draft.street); if (expanded && label.includes(expanded.toLowerCase())) { score += 14; } if ( expanded && /\b\d+\s+[nsew]\s+/i.test(draft.street) && label.includes(expanded.toLowerCase()) === false && labelHasDirection(label, draftDir) ) { score -= 8; } } if (/\bstreet\b/i.test(label)) { score += 4; } if (/\bbridge\b/.test(label) && !/\bbridge\b/i.test(draft.street)) { score -= 20; } return score; } export function sortMothershipCandidatesForDraft( candidates: MothershipAddressCandidate[], draft: { street: string; city: string; state: string }, ): MothershipAddressCandidate[] { return [...candidates].sort( (a, b) => scoreMothershipCandidateForDraft(b, draft) - scoreMothershipCandidateForDraft(a, draft), ); }