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