This commit is contained in:
@@ -7,30 +7,59 @@ const GENERIC_CITY_LABELS = new Set([
|
|||||||
'省直辖县级行政区划'
|
'省直辖县级行政区划'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const NAME_LABEL_REG = /(收货人|收件人|联系人|姓名)/g;
|
const NAME_LABELS = ['收货人', '收件人', '联系人', '姓名'];
|
||||||
const PHONE_LABEL_REG = /(手机号码|手机号|联系电话|联系方式|电话|手机)/g;
|
const PHONE_LABELS = ['手机号码', '手机号', '联系电话', '联系方式', '电话', '手机'];
|
||||||
const ADDRESS_LABEL_REG = /(收货地址|详细地址|地址|所在地区|地区)/g;
|
const REGION_LABELS = ['所在地区', '地区'];
|
||||||
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
|
const DETAIL_LABELS = ['详细地址', '收货地址', '地址'];
|
||||||
|
const ALL_FIELD_LABELS = [
|
||||||
|
...new Set([...NAME_LABELS, ...PHONE_LABELS, ...REGION_LABELS, ...DETAIL_LABELS])
|
||||||
|
];
|
||||||
|
|
||||||
const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/;
|
const PHONE_REG = /(?<!\d)(?:\+?86[-\s]?)?(1[3-9](?:[\s-]?\d){9})(?!\d)/;
|
||||||
|
const ADDRESS_SIGNAL_REG = /(省|市|区|县|旗|乡|镇|街道|大道|路|街|巷|弄|村|社区|小区|号|栋|单元|室|楼)/;
|
||||||
|
const FIELD_LABEL_PATTERN = createLabelPattern(ALL_FIELD_LABELS);
|
||||||
|
const STRIP_FIELD_LABEL_REG = new RegExp(
|
||||||
|
`(^|[\\s,,;;\\n])(?:${FIELD_LABEL_PATTERN})\\s*[::]`,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
|
||||||
const provinceOptions = regionData.map(({ label, value }) => ({ label, value }));
|
const provinceOptions = regionData.map(({ label, value }) => ({ label, value }));
|
||||||
|
|
||||||
const districtRecords = regionData.flatMap((province) =>
|
const districtRecords = regionData.flatMap((province) =>
|
||||||
(province.children || []).flatMap((city) =>
|
(province.children || []).flatMap((city) =>
|
||||||
(city.children || []).map((district) => ({
|
(city.children || []).map((district) => {
|
||||||
province,
|
const provinceAliases = createProvinceAliases(province.label);
|
||||||
city,
|
const cityAliases = createCityAliases(getDisplayCityLabel(city.label));
|
||||||
district,
|
const districtAliases = createDistrictAliases(district.label);
|
||||||
provinceSearchLabel: normalizeForSearch(province.label),
|
|
||||||
citySearchLabel: normalizeForSearch(getDisplayCityLabel(city.label)),
|
return {
|
||||||
districtSearchLabel: normalizeForSearch(district.label),
|
province,
|
||||||
fullSearchLabel: normalizeForSearch(
|
city,
|
||||||
`${province.label}${getDisplayCityLabel(city.label)}${district.label}`
|
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 = '') {
|
function normalizeText(value = '') {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replace(/\r/g, '\n')
|
.replace(/\r/g, '\n')
|
||||||
@@ -43,6 +72,64 @@ function normalizeForSearch(value = '') {
|
|||||||
return normalizeText(value).replace(/[\s,,.。;;::、\-_/]/g, '');
|
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 = '') {
|
function getDisplayCityLabel(label = '') {
|
||||||
return GENERIC_CITY_LABELS.has(label) ? '' : label;
|
return GENERIC_CITY_LABELS.has(label) ? '' : label;
|
||||||
}
|
}
|
||||||
@@ -90,17 +177,32 @@ function getLabelParts(regionCodes = []) {
|
|||||||
return [province.label, displayCityLabel, district.label].filter(Boolean);
|
return [province.label, displayCityLabel, district.label].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripFieldLabels(value = '') {
|
||||||
|
return normalizeText(value).replace(STRIP_FIELD_LABEL_REG, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupAddressText(value = '') {
|
function cleanupAddressText(value = '') {
|
||||||
return normalizeText(value)
|
return stripFieldLabels(value)
|
||||||
.replace(NAME_LABEL_REG, ' ')
|
|
||||||
.replace(PHONE_LABEL_REG, ' ')
|
|
||||||
.replace(ADDRESS_LABEL_REG, ' ')
|
|
||||||
.replace(/[::]/g, ' ')
|
.replace(/[::]/g, ' ')
|
||||||
.replace(/[,,;;]/g, ' ')
|
.replace(/[,,;;]/g, ' ')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim();
|
.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 = '') {
|
function extractPhone(rawText = '') {
|
||||||
const text = normalizeText(rawText);
|
const text = normalizeText(rawText);
|
||||||
const match = text.match(PHONE_REG);
|
const match = text.match(PHONE_REG);
|
||||||
@@ -125,12 +227,16 @@ function pickBestMatch(records, matcher) {
|
|||||||
.filter(matcher)
|
.filter(matcher)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
return (
|
return (
|
||||||
right.fullSearchLabel.length - left.fullSearchLabel.length ||
|
right.fullSearchLabels[0]?.length - left.fullSearchLabels[0]?.length ||
|
||||||
right.districtSearchLabel.length - left.districtSearchLabel.length
|
right.district.label.length - left.district.label.length
|
||||||
);
|
);
|
||||||
})[0] || null;
|
})[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchAnyAlias(searchText = '', aliases = []) {
|
||||||
|
return aliases.some((alias) => alias && searchText.includes(alias));
|
||||||
|
}
|
||||||
|
|
||||||
function findRegionMatch(rawText = '') {
|
function findRegionMatch(rawText = '') {
|
||||||
const searchText = normalizeForSearch(rawText);
|
const searchText = normalizeForSearch(rawText);
|
||||||
if (!searchText) {
|
if (!searchText) {
|
||||||
@@ -138,16 +244,24 @@ function findRegionMatch(rawText = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let match = pickBestMatch(districtRecords, (record) => {
|
let match = pickBestMatch(districtRecords, (record) => {
|
||||||
return record.fullSearchLabel && searchText.includes(record.fullSearchLabel);
|
return record.fullSearchLabels.some((label) => searchText.includes(label));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
match = pickBestMatch(districtRecords, (record) => {
|
match = pickBestMatch(districtRecords, (record) => {
|
||||||
return (
|
return (
|
||||||
record.provinceSearchLabel &&
|
matchAnyAlias(searchText, record.provinceSearchAliases) &&
|
||||||
record.districtSearchLabel &&
|
matchAnyAlias(searchText, record.districtSearchAliases) &&
|
||||||
searchText.includes(record.provinceSearchLabel) &&
|
(!record.citySearchAliases.length || matchAnyAlias(searchText, record.citySearchAliases))
|
||||||
searchText.includes(record.districtSearchLabel)
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
match = pickBestMatch(districtRecords, (record) => {
|
||||||
|
return (
|
||||||
|
matchAnyAlias(searchText, record.citySearchAliases) &&
|
||||||
|
matchAnyAlias(searchText, record.districtSearchAliases)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -156,8 +270,11 @@ function findRegionMatch(rawText = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRegionStartIndex(text, match) {
|
function getRegionStartIndex(text, match) {
|
||||||
const cityLabel = getDisplayCityLabel(match.city.label);
|
const candidates = [
|
||||||
const candidates = [match.province.label, cityLabel, match.district.label]
|
findBestAliasInText(text, match.provinceAliases),
|
||||||
|
findBestAliasInText(text, match.cityAliases),
|
||||||
|
findBestAliasInText(text, match.districtAliases)
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((label) => text.indexOf(label))
|
.map((label) => text.indexOf(label))
|
||||||
.filter((index) => index >= 0);
|
.filter((index) => index >= 0);
|
||||||
@@ -166,25 +283,26 @@ function getRegionStartIndex(text, match) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stripMatchedRegion(addressText, match) {
|
function stripMatchedRegion(addressText, match) {
|
||||||
const cityLabel = getDisplayCityLabel(match.city.label);
|
|
||||||
let detailAddress = normalizeText(addressText);
|
let detailAddress = normalizeText(addressText);
|
||||||
|
const matchedProvince = findBestAliasInText(detailAddress, match.provinceAliases);
|
||||||
|
const matchedCity = findBestAliasInText(detailAddress, match.cityAliases);
|
||||||
|
const matchedDistrict = findBestAliasInText(detailAddress, match.districtAliases);
|
||||||
|
|
||||||
[match.province.label, cityLabel, match.district.label]
|
[matchedProvince, matchedCity, matchedDistrict]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.forEach((label) => {
|
.forEach((label) => {
|
||||||
detailAddress = detailAddress.replace(label, '');
|
detailAddress = detailAddress.replace(label, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
return detailAddress.replace(/^[,,;;::\s]+/, '').trim();
|
return cleanupAddressText(detailAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) {
|
function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartIndex = -1) {
|
||||||
const sourceText = regionStartIndex > 0
|
const sourceText =
|
||||||
? textWithoutPhone.slice(0, regionStartIndex)
|
regionStartIndex > 0 ? textWithoutPhone.slice(0, regionStartIndex) : textWithoutPhone || rawText;
|
||||||
: textWithoutPhone || rawText;
|
|
||||||
|
|
||||||
const directNameMatch = normalizeText(rawText).match(
|
const directNameMatch = normalizeText(rawText).match(
|
||||||
/(?:收货人|收件人|联系人|姓名)\s*[::]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/
|
/(?:收货人|收件人|联系人|姓名)\s*[::]?\s*([A-Za-z\u4e00-\u9fa5·]{2,20})/u
|
||||||
);
|
);
|
||||||
if (directNameMatch?.[1]) {
|
if (directNameMatch?.[1]) {
|
||||||
return directNameMatch[1].trim();
|
return directNameMatch[1].trim();
|
||||||
@@ -196,7 +314,7 @@ function extractNameFromText(rawText = '', textWithoutPhone = '', regionStartInd
|
|||||||
.trim();
|
.trim();
|
||||||
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
||||||
const nameToken = tokens.find((token) => {
|
const nameToken = tokens.find((token) => {
|
||||||
return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/.test(token);
|
return !ADDRESS_SIGNAL_REG.test(token) && /^[A-Za-z\u4e00-\u9fa5·]{2,20}$/u.test(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
return nameToken || '';
|
return nameToken || '';
|
||||||
@@ -223,6 +341,78 @@ function formatRegionCodes(match) {
|
|||||||
return [match.province.value, match.city.value, match.district.value];
|
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 = []) {
|
export function buildRegionColumns(regionCodes = []) {
|
||||||
const provinceCode =
|
const provinceCode =
|
||||||
getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || '';
|
getProvinceByCode(regionCodes[0])?.value || provinceOptions[0]?.value || '';
|
||||||
@@ -291,6 +481,11 @@ export function parseShippingText(rawText = '') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldBasedResult = parseFieldBasedShippingText(text);
|
||||||
|
if (fieldBasedResult) {
|
||||||
|
return fieldBasedResult;
|
||||||
|
}
|
||||||
|
|
||||||
const { phone, textWithoutPhone } = extractPhone(text);
|
const { phone, textWithoutPhone } = extractPhone(text);
|
||||||
const regionMatch = findRegionMatch(textWithoutPhone);
|
const regionMatch = findRegionMatch(textWithoutPhone);
|
||||||
const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1;
|
const regionStartIndex = regionMatch ? getRegionStartIndex(textWithoutPhone, regionMatch) : -1;
|
||||||
|
|||||||
Reference in New Issue
Block a user