'use strict'; /** * 补齐归属表里缺失的条目(第二轮)。 * * 第一轮对「荔波小七孔」这类名字搜不到——Photon 里收录的是「小七孔」这个景点, * 全名反而搜不到。所以这里做两件事: * 1. 去掉「风景区/景区/公园/自然保护区/度假区」等后缀再搜一次; * 2. 搜不到就用「名字里剥掉修饰词的核心词」再搜。 * 命中后仍然用**坐标距离**在候选里挑最近的,避免同名异地。 * * 用法:node tools/build-admin-fix.cjs */ const fs = require('fs'); const path = require('path'); const cn = require('../lib/places-cn'); const niche = require('../lib/places-niche').NICHE_SPOTS; const PHOTON = 'https://photon.komoot.io/api/'; function haversine(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return 2 * R * Math.asin(Math.min(1, Math.sqrt(a))); } function pickArea(props) { const clean = (s) => String(s || '').trim(); const state = clean(props.state); const county = clean(props.county); const city = clean(props.city); // 优先 county(Photon 里是地级市/州),其次 city(县/区),最后 state const area = county || city || state; const detail = county && city && city !== county ? city : ''; return { area, detail, state, level: county ? 'prefecture' : (city ? 'county' : 'province') }; } /** 名字变体:原名、去后缀、剥掉「XX」前缀的核心词 */ function variants(name) { const out = new Set([name]); const noSuffix = name.replace(/(风景区|景区|旅游区|度假区|自然保护区|森林公园|地质公园|湿地公园|国家公园|风景名胜区|风景区|风景区|景区|景点|公园|博物馆|纪念馆|故居|遗址|石林|峡谷|瀑布|温泉|梯田|草原|湖泊|湿地|海滩|岛|山|湖|园)$/g, '').trim(); if (noSuffix && noSuffix.length >= 2) out.add(noSuffix); // 「A小七孔」→「小七孔」这类:去掉末尾的行政区前缀 const core = noSuffix.replace(/^(.{2,4}?)(?=[\u4e00-\u9fa5]{2,4}$)/, ''); if (core && core.length >= 2) out.add(core); return [...out]; } async function query(name) { for (let i = 0; i < 3; i += 1) { try { const u = new URL(PHOTON); u.searchParams.set('q', name); u.searchParams.set('limit', '6'); u.searchParams.set('lang', 'default'); const res = await fetch(u.toString()); const text = await res.text(); if (!text.startsWith('{')) throw new Error('non-json'); return JSON.parse(text).features || []; } catch (err) { if (i === 2) return null; await new Promise((r) => setTimeout(r, 1200 * (i + 1))); } } return null; } async function lookup(name, lat, lon) { for (const v of variants(name)) { const feats = await query(v); if (!feats || !feats.length) continue; let best = null; for (const f of feats) { const co = f.geometry?.coordinates; if (!co) continue; const d = haversine(lat, lon, Number(co[1]), Number(co[0])); if (!best || d < best.d) best = { d, props: f.properties || {} }; } if (!best || best.d > 40) continue; const area = pickArea(best.props); if (area.area) return area; await new Promise((r) => setTimeout(r, 500)); } return null; } async function main() { const file = path.join(__dirname, '..', 'lib', 'places-admin.js'); const src = fs.readFileSync(file, 'utf8'); const SPOT_AREA = JSON.parse(src.match(/const SPOT_AREA = (\{.*?\});/s)[1]); const AREA_COORD = JSON.parse(src.match(/const AREA_COORD = (\{.*?\});/s)[1]); const items = []; for (const s of cn.EXTRA_SCENIC) items.push({ group: 'scenic', name: s[0], lat: s[1], lon: s[2] }); for (const s of niche) items.push({ group: 'niche', name: s[0], lat: s[1], lon: s[2] }); const missing = items.filter((it) => !SPOT_AREA[`${it.group}:${it.name}`]); console.log('missing', missing.length, '/', items.length); let fixed = 0; for (let i = 0; i < missing.length; i += 1) { const it = missing[i]; const area = await lookup(it.name, it.lat, it.lon); if (area) { SPOT_AREA[`${it.group}:${it.name}`] = area; if (!AREA_COORD[area.area]) AREA_COORD[area.area] = [it.lat, it.lon]; fixed += 1; } if ((i + 1) % 10 === 0) console.log(` ${i + 1}/${missing.length} fixed=${fixed}`); await new Promise((r) => setTimeout(r, 600)); } const body = `'use strict'; /** * 内置地点库的归属地名(自动生成,勿手改)。 * * 生成方式: * tools/build-admin.cjs —— 首轮,Photon 按名检索 + 坐标距离消歧 * tools/build-admin-fix.cjs —— 二轮,去后缀/核心词重试,补首轮搜不到的 * * 字段: * SPOT_AREA { 'scenic:荔波小七孔': { area, detail, state, level } } * AREA_COORD { '荔波县': [lat, lon] } 各地级/县级名的代表坐标,运行时做最近市兜底 */ const SPOT_AREA = ${JSON.stringify(SPOT_AREA)}; const AREA_COORD = ${JSON.stringify(AREA_COORD)}; module.exports = { SPOT_AREA, AREA_COORD }; `; fs.writeFileSync(file, body, 'utf8'); console.log('written', file, 'entries', Object.keys(SPOT_AREA).length, 'fixed', fixed); } main();