*/ public const CITIES = [ '北京' => [39.9075, 116.3972, '北京市'], '上海' => [31.2222, 121.4581, '上海市'], '天津' => [39.1422, 117.1767, '天津市'], '重庆' => [29.5603, 106.5577, '重庆市'], '石家庄' => [38.0414, 114.4786, '河北省'], '唐山' => [39.6438, 118.1832, '河北省'], '保定' => [38.8729, 115.4625, '河北省'], '廊坊' => [39.5208, 116.7147, '河北省'], '秦皇岛' => [39.9410, 119.5894, '河北省'], '沧州' => [38.3112, 116.8533, '河北省'], '邯郸' => [36.6100, 114.4876, '河北省'], '邢台' => [37.0622, 114.4927, '河北省'], '张家口' => [40.7834, 114.8714, '河北省'], '承德' => [40.9519, 117.9588, '河北省'], '衡水' => [37.7391, 115.6835, '河北省'], '太原' => [37.8694, 112.5603, '山西省'], '大同' => [40.0936, 113.2914, '山西省'], '临汾' => [36.0889, 111.5189, '山西省'], '运城' => [35.0231, 110.9928, '山西省'], '长治' => [34.8659, 110.3957, '山西省'], '晋中' => [37.6840, 112.7547, '山西省'], '呼和浩特' => [40.8106, 111.6522, '内蒙古自治区'], '包头' => [40.6516, 109.8439, '内蒙古自治区'], '鄂尔多斯' => [39.6086, 109.7816, '内蒙古自治区'], '赤峰' => [42.2683, 118.9636, '内蒙古自治区'], '沈阳' => [41.7922, 123.4328, '辽宁省'], '大连' => [38.9122, 121.6022, '辽宁省'], '鞍山' => [41.1236, 122.9900, '辽宁省'], '抚顺' => [41.8867, 123.9436, '辽宁省'], '本溪' => [41.2886, 123.7650, '辽宁省'], '丹东' => [40.1292, 124.3947, '辽宁省'], '锦州' => [41.1078, 121.1417, '辽宁省'], '营口' => [40.6647, 122.2318, '辽宁省'], '盘锦' => [41.1210, 122.0739, '辽宁省'], '葫芦岛' => [40.7524, 120.8355, '辽宁省'], '朝阳' => [40.4844, 123.5369, '辽宁省'], '长春' => [43.8800, 125.3228, '吉林省'], '吉林' => [43.8465, 126.5608, '吉林省'], '四平' => [43.1614, 124.3778, '吉林省'], '通化' => [41.7197, 125.9264, '吉林省'], '延吉' => [42.8882, 129.5024, '吉林省'], '哈尔滨' => [45.7500, 126.6500, '黑龙江省'], '齐齐哈尔' => [47.3392, 123.9615, '黑龙江省'], '大庆' => [46.5833, 125.0000, '黑龙江省'], '牡丹江' => [44.5480, 129.6259, '黑龙江省'], '佳木斯' => [46.7971, 130.3112, '黑龙江省'], '南京' => [32.0617, 118.7778, '江苏省'], '苏州' => [31.3041, 120.5954, '江苏省'], '无锡' => [31.5689, 120.2886, '江苏省'], '徐州' => [34.2044, 117.2839, '江苏省'], '常州' => [31.7736, 119.9540, '江苏省'], '南通' => [32.0303, 120.8747, '江苏省'], '扬州' => [32.3972, 119.4358, '江苏省'], '盐城' => [33.3575, 120.1573, '江苏省'], '泰州' => [32.4907, 119.9081, '江苏省'], '镇江' => [32.2109, 119.4551, '江苏省'], '淮安' => [33.5975, 119.0211, '江苏省'], '连云港' => [34.5984, 119.2156, '江苏省'], '宿迁' => [33.9492, 118.2958, '江苏省'], '杭州' => [30.2936, 120.1614, '浙江省'], '宁波' => [29.8782, 121.5494, '浙江省'], '温州' => [27.9994, 120.6668, '浙江省'], '嘉兴' => [30.7522, 120.7500, '浙江省'], '绍兴' => [30.0024, 120.5786, '浙江省'], '台州' => [28.6627, 121.4331, '浙江省'], '金华' => [29.1068, 119.6442, '浙江省'], '湖州' => [30.8703, 120.0933, '浙江省'], '衢州' => [28.9594, 118.8686, '浙江省'], '丽水' => [28.4604, 119.9103, '浙江省'], '舟山' => [29.9887, 122.2049, '浙江省'], '合肥' => [31.8639, 117.2808, '安徽省'], '芜湖' => [31.3526, 118.4295, '安徽省'], '蚌埠' => [32.9408, 117.3608, '安徽省'], '安庆' => [30.5136, 117.0472, '安徽省'], '马鞍山' => [31.6858, 118.5101, '安徽省'], '淮南' => [32.6264, 116.9969, '安徽省'], '黄山' => [29.7114, 118.3125, '安徽省'], '滁州' => [32.3219, 118.2978, '安徽省'], '阜阳' => [32.9000, 115.8167, '安徽省'], '宿州' => [33.6361, 116.9789, '安徽省'], '六安' => [31.7356, 116.5169, '安徽省'], '亳州' => [33.8772, 115.7703, '安徽省'], '福州' => [26.0614, 119.3061, '福建省'], '厦门' => [24.4798, 118.0819, '福建省'], '泉州' => [24.9139, 118.5858, '福建省'], '漳州' => [24.5133, 117.6556, '福建省'], '莆田' => [25.4394, 119.0103, '福建省'], '三明' => [26.2486, 117.6186, '福建省'], '南平' => [26.6450, 118.1736, '福建省'], '龙岩' => [25.0749, 117.0178, '福建省'], '宁德' => [26.6617, 119.5228, '福建省'], '南昌' => [28.6840, 115.8531, '江西省'], '九江' => [29.7048, 116.0021, '江西省'], '赣州' => [25.8466, 114.9326, '江西省'], '景德镇' => [29.2947, 117.2079, '江西省'], '上饶' => [28.4518, 117.9429, '江西省'], '宜春' => [27.8333, 114.4000, '江西省'], '吉安' => [27.1172, 114.9793, '江西省'], '抚州' => [27.9600, 116.3333, '江西省'], '济南' => [36.6683, 116.9972, '山东省'], '青岛' => [36.0649, 120.3804, '山东省'], '烟台' => [37.4765, 121.4408, '山东省'], '威海' => [37.5091, 122.1136, '山东省'], '潍坊' => [36.7100, 119.1019, '山东省'], '淄博' => [36.7906, 118.0633, '山东省'], '临沂' => [35.0631, 118.3428, '山东省'], '济宁' => [35.4050, 116.5814, '山东省'], '泰安' => [36.1853, 117.1200, '山东省'], '日照' => [35.4141, 119.5291, '山东省'], '东营' => [37.4627, 118.4917, '山东省'], '滨州' => [37.3667, 118.0167, '山东省'], '德州' => [37.4466, 116.3671, '山东省'], '聊城' => [36.4506, 116.0025, '山东省'], '菏泽' => [35.2393, 115.4736, '山东省'], '枣庄' => [34.8647, 117.5542, '山东省'], '郑州' => [34.7578, 113.6486, '河南省'], '洛阳' => [34.6735, 112.4368, '河南省'], '开封' => [34.7986, 114.3074, '河南省'], '新乡' => [35.1903, 113.8015, '河南省'], '安阳' => [36.0960, 114.3828, '河南省'], '焦作' => [35.2392, 113.2391, '河南省'], '平顶山' => [33.7309, 113.3155, '河南省'], '许昌' => [34.0319, 113.8630, '河南省'], '漯河' => [33.5639, 114.0427, '河南省'], '三门峡' => [34.7808, 111.1929, '河南省'], '南阳' => [33.0052, 112.5466, '河南省'], '商丘' => [34.4143, 115.6561, '河南省'], '信阳' => [32.1228, 114.0656, '河南省'], '周口' => [33.6333, 114.6333, '河南省'], '驻马店' => [32.9794, 114.0294, '河南省'], '武汉' => [30.5833, 114.2667, '湖北省'], '宜昌' => [30.7144, 111.2847, '湖北省'], '襄阳' => [32.0422, 112.1448, '湖北省'], '荆州' => [30.3503, 112.1903, '湖北省'], '黄石' => [30.2471, 115.0481, '湖北省'], '十堰' => [32.6475, 110.7781, '湖北省'], '孝感' => [30.9269, 113.9222, '湖北省'], '荆门' => [31.0336, 112.2047, '湖北省'], '长沙' => [28.1987, 112.9709, '湖南省'], '株洲' => [27.8333, 113.1500, '湖南省'], '湘潭' => [27.8500, 112.9000, '湖南省'], '衡阳' => [26.8895, 112.6189, '湖南省'], '岳阳' => [29.3745, 113.0948, '湖南省'], '常德' => [29.0321, 111.6984, '湖南省'], '郴州' => [25.8000, 113.0333, '湖南省'], '张家界' => [29.1294, 110.4783, '湖南省'], '广州' => [23.1167, 113.2500, '广东省'], '深圳' => [22.5455, 114.0683, '广东省'], '珠海' => [22.2769, 113.5678, '广东省'], '汕头' => [23.3549, 116.6788, '广东省'], '佛山' => [23.0268, 113.1315, '广东省'], '东莞' => [23.0180, 113.7487, '广东省'], '中山' => [22.5231, 113.3791, '广东省'], '惠州' => [23.1115, 114.4152, '广东省'], '江门' => [22.5833, 113.0833, '广东省'], '湛江' => [21.2339, 110.3875, '广东省'], '肇庆' => [23.0489, 112.4609, '广东省'], '茂名' => [21.6663, 110.9136, '广东省'], '揭阳' => [23.5418, 116.3658, '广东省'], '潮州' => [23.6540, 116.6226, '广东省'], '梅州' => [24.2886, 116.1177, '广东省'], '河源' => [23.7333, 114.6833, '广东省'], '清远' => [23.7000, 113.0333, '广东省'], '韶关' => [24.8000, 113.5833, '广东省'], '阳江' => [21.8556, 111.9627, '广东省'], '云浮' => [22.9279, 112.0381, '广东省'], '南宁' => [22.8167, 108.3167, '广西壮族自治区'], '桂林' => [25.2802, 110.2964, '广西壮族自治区'], '柳州' => [24.3240, 109.4070, '广西壮族自治区'], '北海' => [21.4835, 109.1155, '广西壮族自治区'], '海口' => [20.0342, 110.3465, '海南省'], '三亚' => [18.2543, 109.5095, '海南省'], '成都' => [30.6667, 104.0667, '四川省'], '绵阳' => [31.4678, 104.6817, '四川省'], '德阳' => [31.1302, 104.3820, '四川省'], '宜宾' => [28.7593, 104.6399, '四川省'], '泸州' => [28.8903, 105.4257, '四川省'], '南充' => [30.7951, 106.0847, '四川省'], '乐山' => [29.5623, 103.7639, '四川省'], '贵阳' => [26.5833, 106.7167, '贵州省'], '遵义' => [27.6867, 106.9072, '贵州省'], '昆明' => [25.0389, 102.7183, '云南省'], '大理' => [25.5847, 100.2123, '云南省'], '丽江' => [26.8688, 100.2207, '云南省'], '曲靖' => [25.4833, 103.7833, '云南省'], '拉萨' => [29.6500, 91.1000, '西藏自治区'], '西安' => [34.3378, 108.7026, '陕西省'], '宝鸡' => [34.3678, 107.2370, '陕西省'], '咸阳' => [34.3378, 108.7026, '陕西省'], '渭南' => [34.5035, 109.5089, '陕西省'], '榆林' => [38.2918, 109.7375, '陕西省'], '兰州' => [36.0570, 103.8399, '甘肃省'], '天水' => [34.5795, 105.7424, '甘肃省'], '西宁' => [36.6255, 101.7574, '青海省'], '银川' => [38.4681, 106.2731, '宁夏回族自治区'], '乌鲁木齐' => [43.8010, 87.6005, '新疆维吾尔自治区'], '喀什' => [39.4672, 75.9868, '新疆维吾尔自治区'], ]; /** * 找坐标。返回 ['lat' => '36.0649', 'lon' => '120.3804', 'place' => '山东省青岛'] 或 null。 * * 接受:完整市名(青岛) / 带「市」的(青岛市) / 带省份前缀的(山东省青岛)。 * 不做模糊匹配 —— "猜错城市"比"没有城市"糟得多(会让人以为天气坏了)。 */ public static function lookup(string $q): ?array { $name = self::normalize($q); if ($name === '' || !isset(self::CITIES[$name])) { return null; } [$lat, $lon, $prov] = self::CITIES[$name]; return [ 'lat' => number_format($lat, 4, '.', ''), 'lon' => number_format($lon, 4, '.', ''), 'place' => self::label($prov, $name), ]; } /** * 归一化:去空白 → 去末尾「市」→ 去省份前缀。 * * ⚠️ 顺序很重要:**先**去「市」再去省份前缀。 * 反过来的话「上海市」会被当成"省份=上海市 + 空市名", * 前缀一剥就什么都不剩了 → 直辖市全都查不到(北京/上海/天津/重庆)。 * * ⚠️ 去省份用"已知省名清单"前缀匹配,而不是"取最后两个字": * 「内蒙古」是三个字、「新疆」是两个字,按长度切必然切错。 */ public static function normalize(string $q): string { $q = trim(preg_replace('/\s+/u', '', $q)); if ($q === '') { return ''; } // ① 末尾的「市」去掉(表里存的是不带「市」的名字) if (mb_substr($q, -1, 1, 'UTF-8') === '市' && mb_strlen($q, 'UTF-8') > 1) { $q = (string)mb_substr($q, 0, mb_strlen($q, 'UTF-8') - 1, 'UTF-8'); } // ② 「山东省青岛」/「河北石家庄」这类带省份的写法,去掉省份前缀 foreach (self::PROVINCE_WORDS as $pw) { if ($pw !== '' && mb_strpos($q, $pw) === 0 && mb_strlen($q, 'UTF-8') > mb_strlen($pw, 'UTF-8')) { $q = (string)mb_substr($q, mb_strlen($pw, 'UTF-8'), null, 'UTF-8'); break; } } return $q; } /** * 展示用的地名:省份简称 + 市名(如「山东·青岛」),直辖市只说市名。 * * ⚠️ 判据不能写成「市名只有两个字就是直辖市」——「青岛」「大连」「厦门」 * 「深圳」都是两个字,那样会把它们漏成光秃秃的「青岛」, * 而没人在乎「北京」是不是直辖市,大家在乎的是"我看到的天气是哪儿的"。 */ private static function label(string $prov, string $city): string { $short = self::PROV_SHORT[$prov] ?? ''; if ($short === '') { return $city; // 直辖市 / 认不出的省份:只说市名 } return $short . '·' . $city; } /** 完整省份名 → 简称(直辖市映射到空串 = 不加前缀) */ private const PROV_SHORT = [ '北京市' => '', '天津市' => '', '上海市' => '', '重庆市' => '', '河北省' => '河北', '山西省' => '山西', '辽宁省' => '辽宁', '吉林省' => '吉林', '黑龙江省' => '黑龙江', '江苏省' => '江苏', '浙江省' => '浙江', '安徽省' => '安徽', '福建省' => '福建', '江西省' => '江西', '山东省' => '山东', '河南省' => '河南', '湖北省' => '湖北', '湖南省' => '湖南', '广东省' => '广东', '海南省' => '海南', '四川省' => '四川', '贵州省' => '贵州', '云南省' => '云南', '陕西省' => '陕西', '甘肃省' => '甘肃', '青海省' => '青海', '台湾省' => '台湾', '内蒙古自治区' => '内蒙古', '广西壮族自治区' => '广西', '西藏自治区' => '西藏', '宁夏回族自治区' => '宁夏', '新疆维吾尔自治区' => '新疆', ]; /** 支持"省份前缀"写法的省名清单(长的排前面,避免「山西」被「山」抢先匹配) */ private const PROVINCE_WORDS = [ '新疆维吾尔自治区', '广西壮族自治区', '宁夏回族自治区', '内蒙古自治区', '西藏自治区', '香港特别行政区', '澳门特别行政区', '黑龙江省', '内蒙古', '新疆', '广西', '宁夏', '西藏', '北京市', '天津市', '上海市', '重庆市', '河北省', '山西省', '辽宁省', '吉林省', '江苏省', '浙江省', '安徽省', '福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '海南省', '四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省', '台湾省', ]; }