import { regionData } from 'element-china-area-data'; const GENERIC_CITY_LABELS = new Set([ '市辖区', '县', '自治区直辖县级行政区划', '省直辖县级行政区划' ]); const NAME_LABELS = ['收货人', '收件人', '联系人', '姓名']; const PHONE_LABELS = ['手机号码', '手机号', '联系电话', '联系方式', '电话', '手机']; const REGION_LABELS = ['所在地区', '地区']; const DETAIL_LABELS = ['详细地址', '收货地址', '地址']; const ALL_FIELD_LABELS = [ ...new Set([...NAME_LABELS, ...PHONE_LABELS, ...REGION_LABELS, ...DETAIL_LABELS]) ]; const PHONE_REG = /(? ({ label, value })); const districtRecords = regionData.flatMap((province) => (province.children || []).flatMap((city) => (city.children || []).map((district) => { const provinceAliases = createProvinceAliases(province.label); const cityAliases = createCityAliases(getDisplayCityLabel(city.label)); const districtAliases = createDistrictAliases(district.label); return { province, city, district, provinceAliases, cityAliases, districtAliases, provinceSearchAliases: provinceAliases.map(normalizeForSearch).filter(Boolean), citySearchAliases: cityAliases.map(normalizeForSearch).filter(Boolean), districtSearchAliases: districtAliases.map(normalizeForSearch).filter(Boolean), fullSearchLabels: buildFullSearchLabels(provinceAliases, cityAliases, districtAliases) }; }) ) ); function escapeRegExp(value = '') { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function createLabelPattern(labels = []) { return labels .slice() .sort((left, right) => right.length - left.length) .map((label) => escapeRegExp(label)) .join('|'); } function normalizeText(value = '') { return String(value) .replace(/\r/g, '\n') .replace(/\u00a0/g, ' ') .replace(/\t/g, ' ') .trim(); } function normalizeForSearch(value = '') { return normalizeText(value).replace(/[\s,,.。;;::、\-_/]/g, ''); } function stripProvinceSuffix(label = '') { return normalizeText(label).replace( /(特别行政区|维吾尔自治区|壮族自治区|回族自治区|自治区|省|市)$/u, '' ); } function stripCitySuffix(label = '') { return normalizeText(label).replace(/(自治州|地区|盟|市)$/u, ''); } function stripDistrictSuffix(label = '') { return normalizeText(label).replace(/(自治县|自治旗|区|县|旗|市)$/u, ''); } function createAliases(label = '', stripFn = null) { const rawLabel = normalizeText(label); const aliases = new Set([rawLabel]); const simplifiedLabel = stripFn ? normalizeText(stripFn(rawLabel)) : ''; if (simplifiedLabel && simplifiedLabel !== rawLabel) { aliases.add(simplifiedLabel); } return [...aliases].filter(Boolean); } function createProvinceAliases(label = '') { return createAliases(label, stripProvinceSuffix); } function createCityAliases(label = '') { if (!label) return ['']; return createAliases(label, stripCitySuffix); } function createDistrictAliases(label = '') { return createAliases(label, stripDistrictSuffix); } function buildFullSearchLabels(provinceAliases = [], cityAliases = [], districtAliases = []) { const fullLabels = new Set(); const normalizedCityAliases = cityAliases.length ? cityAliases : ['']; provinceAliases.forEach((provinceAlias) => { normalizedCityAliases.forEach((cityAlias) => { districtAliases.forEach((districtAlias) => { const combinedLabel = normalizeForSearch(`${provinceAlias}${cityAlias}${districtAlias}`); if (combinedLabel) { fullLabels.add(combinedLabel); } }); }); }); return [...fullLabels]; } function getDisplayCityLabel(label = '') { return GENERIC_CITY_LABELS.has(label) ? '' : label; } function getProvinceByCode(provinceCode) { return regionData.find((item) => item.value === provinceCode) || null; } function getCityByCode(provinceCode, cityCode) { const province = getProvinceByCode(provinceCode); return province?.children?.find((item) => item.value === cityCode) || null; } function getDistrictByCode(provinceCode, cityCode, districtCode) { const city = getCityByCode(provinceCode, cityCode); return city?.children?.find((item) => item.value === districtCode) || null; } function getCityOptions(provinceCode) { const province = getProvinceByCode(provinceCode) || regionData[0]; return (province?.children || []).map(({ label, value }) => ({ label, value })); } function getDistrictOptions(provinceCode, cityCode) { const province = getProvinceByCode(provinceCode) || regionData[0]; const city = province?.children?.find((item) => item.value === cityCode) || province?.children?.[0] || null; return (city?.children || []).map(({ label, value }) => ({ label, value })); } function getLabelParts(regionCodes = []) { const [provinceCode, cityCode, districtCode] = regionCodes; const province = getProvinceByCode(provinceCode); const city = getCityByCode(provinceCode, cityCode); const district = getDistrictByCode(provinceCode, cityCode, districtCode); if (!province || !city || !district) { return []; } const displayCityLabel = getDisplayCityLabel(city.label); return [province.label, displayCityLabel, district.label].filter(Boolean); } function stripFieldLabels(value = '') { return normalizeText(value).replace(STRIP_FIELD_LABEL_REG, '$1'); } function cleanupAddressText(value = '') { return stripFieldLabels(value) .replace(/[::]/g, ' ') .replace(/[,,;;]/g, ' ') .replace(/\s+/g, ' ') .trim(); } function cleanupName(value = '') { return normalizeText(value) .replace(/^[\s,,;;::]+/, '') .replace(/[\s,,;;::]+$/, '') .trim(); } function findBestAliasInText(text = '', aliases = []) { return aliases .slice() .sort((left, right) => right.length - left.length) .find((alias) => alias && text.includes(alias)) || ''; } function extractPhone(rawText = '') { const text = normalizeText(rawText); const match = text.match(PHONE_REG); if (!match) { return { phone: '', textWithoutPhone: text }; } const digits = match[1].replace(/\D/g, ''); const phone = digits.length > 11 ? digits.slice(-11) : digits; return { phone, textWithoutPhone: text.replace(match[0], ' ').replace(/\s+/g, ' ').trim() }; } function pickBestMatch(records, matcher) { return records .filter(matcher) .sort((left, right) => { return ( right.fullSearchLabels[0]?.length - left.fullSearchLabels[0]?.length || right.district.label.length - left.district.label.length ); })[0] || null; } function matchAnyAlias(searchText = '', aliases = []) { return aliases.some((alias) => alias && searchText.includes(alias)); } function findRegionMatch(rawText = '') { const searchText = normalizeForSearch(rawText); if (!searchText) { return null; } let match = pickBestMatch(districtRecords, (record) => { return record.fullSearchLabels.some((label) => searchText.includes(label)); }); if (!match) { match = pickBestMatch(districtRecords, (record) => { return ( matchAnyAlias(searchText, record.provinceSearchAliases) && matchAnyAlias(searchText, record.districtSearchAliases) && (!record.citySearchAliases.length || matchAnyAlias(searchText, record.citySearchAliases)) ); }); } if (!match) { match = pickBestMatch(districtRecords, (record) => { return ( matchAnyAlias(searchText, record.citySearchAliases) && matchAnyAlias(searchText, record.districtSearchAliases) ); }); } return match; } function getRegionStartIndex(text, match) { const candidates = [ findBestAliasInText(text, match.provinceAliases), findBestAliasInText(text, match.cityAliases), findBestAliasInText(text, match.districtAliases) ] .filter(Boolean) .map((label) => text.indexOf(label)) .filter((index) => index >= 0); return candidates.length ? Math.min(...candidates) : -1; } function stripMatchedRegion(addressText, match) { let detailAddress = normalizeText(addressText); const matchedProvince = findBestAliasInText(detailAddress, match.provinceAliases); const matchedCity = findBestAliasInText(detailAddress, match.cityAliases); const matchedDistrict = findBestAliasInText(detailAddress, match.districtAliases); [matchedProvince, matchedCity, matchedDistrict] .filter(Boolean) .forEach((label) => { detailAddress = detailAddress.replace(label, ''); }); return cleanupAddressText(detailAddress); } function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) { const sourceText = regionStartIndex > 0 ? textWithoutPhone.slice(0, regionStartIndex) : textWithoutPhone || rawText; const directNameMatch = normalizeText(rawText).match( /(?:收货人|收件人|联系人|姓名)\s*[::]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/u ); if (directNameMatch?.[1]) { return directNameMatch[1].trim(); } const cleaned = cleanupAddressText(sourceText) .replace(PHONE_REG, ' ') .replace(/\d+/g, ' ') .trim(); const tokens = cleaned.split(/\s+/).filter(Boolean); const nameToken = tokens.find((token) => { return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/u.test(token); }); return nameToken || ''; } function extractAddressSource(textWithoutPhone = '', name = '', match = null) { let addressSource = normalizeText(textWithoutPhone); if (name) { addressSource = addressSource.replace(name, ' ').replace(/\s+/g, ' ').trim(); } if (match) { const regionStartIndex = getRegionStartIndex(addressSource, match); if (regionStartIndex >= 0) { return addressSource.slice(regionStartIndex).trim(); } } return cleanupAddressText(addressSource); } function formatRegionCodes(match) { return [match.province.value, match.city.value, match.district.value]; } function combineDetailParts(parts = []) { return parts .map((item) => cleanupAddressText(item)) .filter(Boolean) .join(''); } function extractFieldValue(text = '', labels = []) { if (!text || !labels.length) { return ''; } const labelPattern = createLabelPattern(labels); const fieldReg = new RegExp( `(?:${labelPattern})\\s*[::]\\s*([\\s\\S]*?)(?=(?:[\\n,,]\\s*)?(?:${FIELD_LABEL_PATTERN})\\s*[::]|$)`, 'u' ); const match = normalizeText(text).match(fieldReg); if (!match?.[1]) { return ''; } return normalizeText(match[1]).replace(/^[,,\s]+|[,,\s]+$/g, ''); } function extractLabeledFields(text = '') { return { name: extractFieldValue(text, NAME_LABELS), phone: extractFieldValue(text, PHONE_LABELS), region: extractFieldValue(text, REGION_LABELS), detail: extractFieldValue(text, DETAIL_LABELS) }; } function parseFieldBasedShippingText(text = '') { const fields = extractLabeledFields(text); const hasAnyField = Object.values(fields).some(Boolean); if (!hasAnyField) { return null; } const recipientName = cleanupName(fields.name); const recipientPhone = extractPhone(fields.phone || text).phone; const regionSource = normalizeText(fields.region); const detailSource = normalizeText(fields.detail); let regionMatch = findRegionMatch(regionSource || `${detailSource}${text}`); if (!regionMatch) { regionMatch = findRegionMatch(text); } const detailParts = []; if (regionSource) { const regionRemainder = regionMatch ? stripMatchedRegion(regionSource, regionMatch) : regionSource; if (regionRemainder) { detailParts.push(regionRemainder); } } if (detailSource) { detailParts.push(detailSource); } return { recipient_name: recipientName, recipient_phone: recipientPhone, regionCodes: regionMatch ? formatRegionCodes(regionMatch) : [], detailAddress: combineDetailParts(detailParts) }; } export function buildRegionColumns(regionCodes = []) { const provinceCode = getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || ''; const cityOptions = getCityOptions(provinceCode); const cityCode = cityOptions.find((item) => item.value === regionCodes[1])?.value || cityOptions[0]?.value || ''; const districtOptions = getDistrictOptions(provinceCode, cityCode); const districtCode = districtOptions.find((item) => item.value === regionCodes[2])?.value || districtOptions[0]?.value || ''; return { columns: [provinceOptions, cityOptions, districtOptions], defaultIndex: [ provinceOptions.findIndex((item) => item.value === provinceCode), cityOptions.findIndex((item) => item.value === cityCode), districtOptions.findIndex((item) => item.value === districtCode) ] }; } export function getRegionDisplayText(regionCodes = []) { return getLabelParts(regionCodes).join(' / '); } export function composeRecipientAddress(regionCodes = [], detailAddress = '') { const regionText = getLabelParts(regionCodes).join(''); return `${regionText}${normalizeText(detailAddress)}`.trim(); } export function parseSavedRecipientAddress(rawAddress = '') { const addressText = normalizeText(rawAddress); if (!addressText) { return { regionCodes: [], detailAddress: '' }; } const match = findRegionMatch(addressText); if (!match) { return { regionCodes: [], detailAddress: addressText }; } const detailAddress = stripMatchedRegion(addressText, match); return { regionCodes: formatRegionCodes(match), detailAddress: detailAddress || addressText }; } export function parseShippingText(rawText = '') { const text = normalizeText(rawText); if (!text) { return { recipient_name: '', recipient_phone: '', regionCodes: [], detailAddress: '' }; } const fieldBasedResult = parseFieldBasedShippingText(text); if (fieldBasedResult) { return fieldBasedResult; } const { phone, textWithoutPhone } = extractPhone(text); const regionMatch = findRegionMatch(textWithoutPhone); const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1; const recipientName = extractNameFromText(text, textWithoutPhone, regionStartIndex); const addressSource = extractAddressSource(textWithoutPhone, recipientName, regionMatch); const detailAddress = regionMatch ? stripMatchedRegion(addressSource, regionMatch) : cleanupAddressText(addressSource); return { recipient_name: recipientName, recipient_phone: phone, regionCodes: regionMatch ? formatRegionCodes(regionMatch) : [], detailAddress }; }