package natal import ( "strings" "unicode/utf8" ) // City is a built-in birth place with WGS84 coordinates. type City struct { Name string Lat float64 Lng float64 } // Cities lists common CN cities for MVP geocode (no network). var Cities = []City{ {"北京", 39.9042, 116.4074}, {"上海", 31.2304, 121.4737}, {"广州", 23.1291, 113.2644}, {"深圳", 22.5431, 114.0579}, {"杭州", 30.2741, 120.1551}, {"成都", 30.5728, 104.0668}, {"重庆", 29.5630, 106.5516}, {"武汉", 30.5928, 114.3055}, {"西安", 34.3416, 108.9398}, {"南京", 32.0603, 118.7969}, {"天津", 39.3434, 117.3616}, {"苏州", 31.2989, 120.5853}, {"长沙", 28.2282, 112.9388}, {"郑州", 34.7466, 113.6254}, {"青岛", 36.0671, 120.3826}, {"大连", 38.9140, 121.6147}, {"厦门", 24.4798, 118.0894}, {"福州", 26.0745, 119.2965}, {"昆明", 25.0389, 102.7183}, {"贵阳", 26.6470, 106.6302}, {"南宁", 22.8170, 108.3665}, {"海口", 20.0440, 110.1999}, {"哈尔滨", 45.8038, 126.5349}, {"长春", 43.8171, 125.3235}, {"沈阳", 41.8057, 123.4315}, {"石家庄", 38.0428, 114.5149}, {"太原", 37.8706, 112.5489}, {"济南", 36.6512, 117.1201}, {"合肥", 31.8206, 117.2272}, {"南昌", 28.6820, 115.8579}, {"兰州", 36.0611, 103.8343}, {"乌鲁木齐", 43.8256, 87.6168}, {"拉萨", 29.6520, 91.1721}, {"呼和浩特", 40.8414, 111.7519}, {"银川", 38.4872, 106.2309}, {"西宁", 36.6171, 101.7782}, {"香港", 22.3193, 114.1694}, {"澳门", 22.1987, 113.5439}, {"台北", 25.0330, 121.5654}, {"宁波", 29.8683, 121.5440}, {"无锡", 31.4912, 120.3119}, {"佛山", 23.0215, 113.1214}, {"东莞", 23.0207, 113.7518}, {"温州", 27.9943, 120.6994}, {"泉州", 24.8741, 118.6759}, {"珠海", 22.2710, 113.5767}, } // DefaultCoords is used when place is missing (东八区中部近似). const DefaultLat = 30.0 const DefaultLng = 114.0 // ResolvePlace returns lat/lng and whether a named city matched. // Accepts plain city ("杭州") or 省市区 ("浙江省 杭州市 西湖区" / "北京市 市辖区 朝阳区"). func ResolvePlace(place string) (lat, lng float64, matched string, ok bool) { place = strings.TrimSpace(place) if place == "" { return DefaultLat, DefaultLng, "", false } // exact city table hit for _, c := range Cities { if c.Name == place { return c.Lat, c.Lng, c.Name, true } } tokens := splitPlace(place) // prefer matching city-level tokens (usually 2nd), then others order := append([]string{}, tokens...) if len(tokens) >= 2 { order = append([]string{tokens[1]}, tokens[0]) if len(tokens) >= 3 { order = append(order, tokens[2:]...) } } for _, tok := range order { if tok == "" || tok == "市辖区" || tok == "县" { continue } key := normalizeAdmin(tok) for _, c := range Cities { if c.Name == key || strings.Contains(tok, c.Name) || strings.Contains(c.Name, key) { return c.Lat, c.Lng, c.Name, true } } } // province-only fallback: use first token city if 直辖市 if len(tokens) > 0 { key := normalizeAdmin(tokens[0]) for _, c := range Cities { if c.Name == key { return c.Lat, c.Lng, c.Name, true } } } return DefaultLat, DefaultLng, "", false } func splitPlace(s string) []string { s = strings.ReplaceAll(s, "/", " ") s = strings.ReplaceAll(s, "/", " ") s = strings.ReplaceAll(s, ",", " ") s = strings.ReplaceAll(s, ",", " ") parts := strings.Fields(s) out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } func normalizeAdmin(s string) string { s = strings.TrimSpace(s) suffixes := []string{"特别行政区", "维吾尔自治区", "壮族自治区", "回族自治区", "自治区", "省", "市", "地区", "盟"} for _, suf := range suffixes { if strings.HasSuffix(s, suf) && utf8.RuneCountInString(s) > utf8.RuneCountInString(suf) { return strings.TrimSuffix(s, suf) } } return s } // CityNames returns names for API/UI pickers. func CityNames() []string { out := make([]string, len(Cities)) for i, c := range Cities { out[i] = c.Name } return out }