|
|
@@ -1,143 +0,0 @@
|
|
|
-// province-city.helper.ts
|
|
|
-
|
|
|
-const MUNICIPALITIES = new Set(['北京', '上海', '天津', '重庆']);
|
|
|
-const SARS = new Set(['香港', '澳门']);
|
|
|
-
|
|
|
-export function normalizeName(s?: string | null): string {
|
|
|
- if (!s) return '';
|
|
|
- let n = String(s).trim();
|
|
|
-
|
|
|
- // squeeze spaces
|
|
|
- n = n.replace(/\s+/g, '');
|
|
|
-
|
|
|
- // remove specific long suffixes first
|
|
|
- n = n
|
|
|
- .replace(/(市辖区)$/u, '')
|
|
|
- .replace(/(自治州)$/u, '') // keep this
|
|
|
- .replace(/(自治县)$/u, '')
|
|
|
- .replace(/(地区)$/u, '')
|
|
|
- .replace(/(自治区)$/u, '')
|
|
|
- .replace(/(特别行政区)$/u, '');
|
|
|
-
|
|
|
- // IMPORTANT: do NOT strip generic "州"
|
|
|
- // only remove 市 / 盟 / 区 / 县
|
|
|
- n = n.replace(/(市|盟|区|县)$/u, '');
|
|
|
-
|
|
|
- return n.trim();
|
|
|
-}
|
|
|
-
|
|
|
-function buildCityToProvinceIndex(provinceCityMap: Record<string, string[]>) {
|
|
|
- const cityToProv = new Map<string, string>();
|
|
|
- for (const [prov, cities] of Object.entries(provinceCityMap)) {
|
|
|
- const np = normalizeName(prov);
|
|
|
- const allowed = (cities || []).map((c) => normalizeName(c)).filter(Boolean);
|
|
|
- for (const nc of allowed) {
|
|
|
- if (!cityToProv.has(nc)) cityToProv.set(nc, np);
|
|
|
- }
|
|
|
- if (MUNICIPALITIES.has(np) || SARS.has(np)) {
|
|
|
- if (!cityToProv.has(np)) cityToProv.set(np, np);
|
|
|
- }
|
|
|
- }
|
|
|
- return cityToProv;
|
|
|
-}
|
|
|
-
|
|
|
-let _memoKey = '';
|
|
|
-let _memoIdx: Map<string, string> | null = null;
|
|
|
-function getCityIdx(provinceCityMap: Record<string, string[]>) {
|
|
|
- const key = JSON.stringify(Object.keys(provinceCityMap));
|
|
|
- if (!_memoIdx || _memoKey !== key) {
|
|
|
- _memoIdx = buildCityToProvinceIndex(provinceCityMap);
|
|
|
- _memoKey = key;
|
|
|
- }
|
|
|
- return _memoIdx!;
|
|
|
-}
|
|
|
-
|
|
|
-function matchCityInProvince(
|
|
|
- nCity: string,
|
|
|
- allowedCities: string[],
|
|
|
-): string | null {
|
|
|
- if (!nCity) return null;
|
|
|
- // allowedCities expected normalized
|
|
|
- if (allowedCities.includes(nCity)) return nCity;
|
|
|
- const hit = allowedCities.find((c) => nCity.includes(c) || c.includes(nCity));
|
|
|
- return hit ?? null;
|
|
|
-}
|
|
|
-
|
|
|
-/** Returns a *normalized* pair */
|
|
|
-export function resolveProvinceCity(
|
|
|
- incoming: { city?: string | null; province?: string | null },
|
|
|
- provinceCityMap: Record<string, string[]>,
|
|
|
-): { province: string; city: string } | null {
|
|
|
- const cityIdx = getCityIdx(provinceCityMap);
|
|
|
-
|
|
|
- const nCity = normalizeName(incoming.city ?? '');
|
|
|
- const nProv = normalizeName(incoming.province ?? '');
|
|
|
-
|
|
|
- // 全国 (only if both are 全国 or province empty)
|
|
|
- if (nCity === '全国' && (!nProv || nProv === '全国')) {
|
|
|
- return { province: '全国', city: '全国' };
|
|
|
- }
|
|
|
-
|
|
|
- // prefer provided province; else infer from city
|
|
|
- const provExists = !!nProv && !!provinceCityMap[nProv];
|
|
|
- const inferredProv = nCity ? cityIdx.get(nCity) : undefined;
|
|
|
- const finalProvince = provExists ? nProv : inferredProv;
|
|
|
- if (!finalProvince) return null;
|
|
|
-
|
|
|
- const allowedCities = (provinceCityMap[finalProvince] || []).map(
|
|
|
- normalizeName,
|
|
|
- );
|
|
|
-
|
|
|
- // Municipality / SAR → city == province
|
|
|
- if (MUNICIPALITIES.has(finalProvince) || SARS.has(finalProvince)) {
|
|
|
- return { province: finalProvince, city: finalProvince };
|
|
|
- }
|
|
|
-
|
|
|
- if (nCity) {
|
|
|
- const matched = matchCityInProvince(nCity, allowedCities);
|
|
|
- if (matched) return { province: finalProvince, city: matched };
|
|
|
- // a city was supplied but didn’t match → don’t downgrade to 全国
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // no city provided → allow province-level 全国 if present
|
|
|
- if (allowedCities.includes('全国')) {
|
|
|
- return { province: finalProvince, city: '全国' };
|
|
|
- }
|
|
|
-
|
|
|
- // no city; keep empty city (or return null if you prefer)
|
|
|
- return { province: finalProvince, city: '' as any };
|
|
|
-}
|
|
|
-
|
|
|
-/** Map normalized to canonical labels present in your map */
|
|
|
-export function toCanonicalProvinceCity(
|
|
|
- resolved: { province: string; city: string } | null,
|
|
|
- provinceCityMap: Record<string, string[]>,
|
|
|
-): { province: string; city: string } | null {
|
|
|
- if (!resolved) return null;
|
|
|
-
|
|
|
- const nProv = normalizeName(resolved.province);
|
|
|
- const nCity = normalizeName(resolved.city);
|
|
|
-
|
|
|
- const provKey = Object.keys(provinceCityMap).find(
|
|
|
- (p) => normalizeName(p) === nProv,
|
|
|
- );
|
|
|
- if (!provKey) return null;
|
|
|
-
|
|
|
- if (MUNICIPALITIES.has(nProv) || SARS.has(nProv)) {
|
|
|
- return { province: provKey, city: provKey };
|
|
|
- }
|
|
|
-
|
|
|
- const cities = provinceCityMap[provKey] ?? [];
|
|
|
- const found =
|
|
|
- cities.find((c) => normalizeName(c) === nCity) ??
|
|
|
- cities.find((c) => {
|
|
|
- const nc = normalizeName(c);
|
|
|
- return nCity.includes(nc) || nc.includes(nCity);
|
|
|
- }) ??
|
|
|
- (nCity === '全国' && cities.some((c) => normalizeName(c) === '全国')
|
|
|
- ? '全国'
|
|
|
- : null);
|
|
|
-
|
|
|
- return { province: provKey, city: found ?? resolved.city };
|
|
|
-}
|