package web import "strings" // 地点补全的判定规则:地名怎么猜活动、高德 POI 类型怎么猜活动和类别。 // // 两条原则: // - **只填空**:调用方只在原值为空时才用这里的建议; // - **拿不准就别猜**:宁可标成「待确认」让人看一眼,也不要瞎填一个看起来像样的值, // 错的活动会直接污染统计与筛选,比空着更难发现。 // // 词表按**定义顺序**匹配,先命中的赢——所以具体的词要排在笼统的词前面。 // actHint 是一条「关键词 → 活动类型」的映射,关键词出现在地名里就算命中。 type actHint struct{ kw, val string } // nameActHints 从地名猜活动。活动名必须与站点里已有的 31 个完全一致, // 否则调用方会把它当成「需要新建的类型」。 var nameActHints = []actHint{ {"咖啡", "奶茶和甜甜水"}, {"奶茶", "奶茶和甜甜水"}, {"茶艺", "奶茶和甜甜水"}, {"书店", "学习"}, {"图书馆", "学习"}, {"学校", "学习"}, {"大学", "学习"}, {"博物馆", "纪念馆"}, {"纪念馆", "纪念馆"}, {"陈列馆", "纪念馆"}, {"动物园", "动物园"}, {"海洋馆", "动物园"}, {"沙滩", "沙滩"}, {"海滨", "沙滩"}, {"海滩", "沙滩"}, {"度假区", "旅游景点"}, {"古镇", "旅游景点"}, {"景区", "旅游景点"}, {"游客中心", "旅游景点"}, {"索道", "旅游景点"}, {"公园", "散步"}, {"绿道", "散步"}, {"步道", "散步"}, {"广场", "散步"}, {"栈道", "散步"}, {"步行街", "步行街"}, {"老街", "步行街"}, {"小吃", "美食街"}, {"美食", "美食街"}, {"夜市", "美食街"}, {"大排档", "美食街"}, {"餐厅", "餐厅"}, {"饭店", "餐厅"}, {"酒楼", "餐厅"}, {"食府", "餐厅"}, {"火锅", "餐厅"}, {"菜市场", "市场"}, {"农贸", "市场"}, {"市场", "市场"}, {"超市", "小商店"}, {"便利店", "小商店"}, {"小卖部", "小商店"}, {"商店", "小商店"}, {"商场", "商场"}, {"购物", "商场"}, {"百货", "商场"}, {"公交", "公共交通"}, {"地铁", "公共交通"}, {"车站", "公共交通"}, {"机场", "公共交通"}, {"高铁", "公共交通"}, {"码头", "公共交通"}, {"客运", "公共交通"}, {"民宿", "酒店"}, {"客栈", "酒店"}, {"宾馆", "酒店"}, {"酒店", "酒店"}, {"度假", "酒店"}, {"医院", "一般公共场所"}, {"诊所", "一般公共场所"}, {"政务", "一般公共场所"}, {"服务中心", "一般公共场所"}, {"体育", "户外活动"}, {"运动", "户外活动"}, {"健身", "户外活动"}, {"露营", "户外活动"}, {"骑行", "骑行目的地"}, {"自行车", "骑行目的地"}, {"租车", "租车"}, {"汽车租赁", "租车"}, {"彩票", "彩票购买点"}, {"4S", "车辆维修"}, {"维修", "车辆维修"}, {"汽修", "车辆维修"}, {"停车", "暂且休息"}, {"服务区", "暂且休息"}, {"休息区", "暂且休息"}, {"公司", "工作"}, {"办公", "工作"}, {"产业园", "工作"}, {"工厂", "工作"}, {"塔", "地标"}, {"大桥", "地标"}, {"地标", "地标"}, {"牌坊", "地标"}, {"乡镇", "乡镇"}, {"村委会", "乡镇"}, {"街道办", "乡镇"}, {"山峰", "山"}, {"峡谷", "山"}, {"雪山", "山"}, } // amapActHints 从高德 POI 类型猜活动(高德给的是「大类;小类」形式,按子串匹配)。 var amapActHints = []actHint{ {"咖啡厅", "奶茶和甜甜水"}, {"茶艺馆", "奶茶和甜甜水"}, {"冷饮店", "奶茶和甜甜水"}, {"图书馆", "学习"}, {"学校", "学习"}, {"高等院校", "学习"}, {"中学", "学习"}, {"小学", "学习"}, {"博物馆", "纪念馆"}, {"展览馆", "纪念馆"}, {"科教文化", "纪念馆"}, {"动物园", "动物园"}, {"水族馆", "动物园"}, {"海滩", "沙滩"}, {"海滨浴场", "沙滩"}, {"风景名胜", "旅游景点"}, {"公园广场", "旅游景点"}, {"寺庙道观", "旅游景点"}, {"公园", "散步"}, {"城市广场", "散步"}, {"特色街区", "步行街"}, {"小吃快餐", "美食街"}, {"美食街", "美食街"}, {"餐饮相关", "美食街"}, {"中餐厅", "餐厅"}, {"外国餐厅", "餐厅"}, {"餐厅", "餐厅"}, {"快餐厅", "餐厅"}, {"综合市场", "市场"}, {"农副产品市场", "市场"}, {"菜市场", "市场"}, {"便利店", "小商店"}, {"超市", "小商店"}, {"专卖店", "小商店"}, {"小商品市场", "小商店"}, {"商场", "商场"}, {"购物中心", "商场"}, {"百货商场", "商场"}, {"地铁站", "公共交通"}, {"公交车站", "公共交通"}, {"火车站", "公共交通"}, {"机场", "公共交通"}, {"港口码头", "公共交通"}, {"长途汽车站", "公共交通"}, {"宾馆酒店", "酒店"}, {"旅馆招待所", "酒店"}, {"度假村", "酒店"}, {"综合医院", "一般公共场所"}, {"专科医院", "一般公共场所"}, {"诊所", "一般公共场所"}, {"政府机关", "一般公共场所"}, {"社会团体", "一般公共场所"}, {"运动场馆", "户外活动"}, {"健身中心", "户外活动"}, {"露营地", "户外活动"}, {"汽车租赁", "租车"}, {"汽车维修", "车辆维修"}, {"汽车服务", "车辆维修"}, {"彩票", "彩票购买点"}, {"停车场", "暂且休息"}, {"服务区", "暂且休息"}, {"公司", "工作"}, {"写字楼", "工作"}, {"产业园区", "工作"}, {"旅游景点", "旅游景点"}, {"商务住宅", "家"}, {"住宅区", "家"}, // 一级大类兜底 {"餐饮服务", "餐厅"}, {"购物服务", "商场"}, {"住宿服务", "酒店"}, {"交通设施服务", "公共交通"}, } // mkHints 从高德 POI 类型猜 MapKit 类别。 // // 值必须是真机能认的枚举名(`MKPOICategory` + 驼峰)。已确认真机用过的是这 12 种: // Restaurant / Store / PublicTransport / Hotel / Landmark / FoodMarket / Beach / // Museum / Campground / Zoo / Stadium / Park。其余取 MapKit 的公开枚举名, // 万一写错,rond 那边只会显示成原始串,不会损坏数据。 var mkHints = []actHint{ {"咖啡厅", "MKPOICategoryCafe"}, {"茶艺馆", "MKPOICategoryCafe"}, {"冷饮店", "MKPOICategoryCafe"}, {"糕饼店", "MKPOICategoryBakery"}, {"面包", "MKPOICategoryBakery"}, {"图书馆", "MKPOICategoryLibrary"}, {"学校", "MKPOICategorySchool"}, {"中学", "MKPOICategorySchool"}, {"小学", "MKPOICategorySchool"}, {"高等院校", "MKPOICategoryUniversity"}, {"博物馆", "MKPOICategoryMuseum"}, {"展览馆", "MKPOICategoryMuseum"}, {"动物园", "MKPOICategoryZoo"}, {"水族馆", "MKPOICategoryAquarium"}, {"海滩", "MKPOICategoryBeach"}, {"海滨浴场", "MKPOICategoryBeach"}, {"风景名胜", "MKPOICategoryLandmark"}, {"寺庙道观", "MKPOICategoryLandmark"}, {"公园", "MKPOICategoryPark"}, {"城市广场", "MKPOICategoryLandmark"}, {"露营地", "MKPOICategoryCampground"}, {"综合市场", "MKPOICategoryFoodMarket"}, {"农副产品市场", "MKPOICategoryFoodMarket"}, {"便利店", "MKPOICategoryStore"}, {"超市", "MKPOICategoryStore"}, {"专卖店", "MKPOICategoryStore"}, {"商场", "MKPOICategoryStore"}, {"购物中心", "MKPOICategoryStore"}, {"地铁站", "MKPOICategoryPublicTransport"}, {"公交车站", "MKPOICategoryPublicTransport"}, {"火车站", "MKPOICategoryPublicTransport"}, {"港口码头", "MKPOICategoryPublicTransport"}, {"长途汽车站", "MKPOICategoryPublicTransport"}, {"机场", "MKPOICategoryAirport"}, {"宾馆酒店", "MKPOICategoryHotel"}, {"旅馆招待所", "MKPOICategoryHotel"}, {"度假村", "MKPOICategoryHotel"}, {"综合医院", "MKPOICategoryHospital"}, {"专科医院", "MKPOICategoryHospital"}, {"诊所", "MKPOICategoryHospital"}, {"药房", "MKPOICategoryPharmacy"}, {"运动场馆", "MKPOICategoryStadium"}, {"健身中心", "MKPOICategoryFitnessCenter"}, {"电影院", "MKPOICategoryMovieTheater"}, {"剧场", "MKPOICategoryTheater"}, {"汽车租赁", "MKPOICategoryCarRental"}, {"加油站", "MKPOICategoryGasStation"}, {"停车场", "MKPOICategoryParking"}, {"银行", "MKPOICategoryBank"}, {"自动取款机", "MKPOICategoryAtm"}, {"邮局", "MKPOICategoryPostOffice"}, {"派出所", "MKPOICategoryPolice"}, {"中餐厅", "MKPOICategoryRestaurant"}, {"外国餐厅", "MKPOICategoryRestaurant"}, {"快餐厅", "MKPOICategoryRestaurant"}, {"餐厅", "MKPOICategoryRestaurant"}, // 高德的一级大类兜底:小类不认识时至少落到大类上 {"餐饮服务", "MKPOICategoryRestaurant"}, {"购物服务", "MKPOICategoryStore"}, {"住宿服务", "MKPOICategoryHotel"}, {"体育休闲服务", "MKPOICategoryStadium"}, {"医疗保健服务", "MKPOICategoryHospital"}, {"风景名胜", "MKPOICategoryLandmark"}, {"交通设施服务", "MKPOICategoryPublicTransport"}, {"金融保险服务", "MKPOICategoryBank"}, } // trimParens 只看括号外的主体:「尚优居客栈(大理古城步行街店)」是个客栈, // 括号里那串只是地址,不该拿它来判断。 func trimParens(s string) string { if i := strings.IndexAny(s, "(([【"); i > 0 { return s[:i] } return s } // matchHint 取**最长的**命中词——越长的词越具体。 // // 不用「定义顺序」也不用「出现位置」:前者要求把词表排成一件容易出错的事, // 后者会让「马鞍山公园」命中「山」而不是「公园」。长度相同则取先定义的。 func matchHint(hints []actHint, s string) string { if s == "" { return "" } best, bestLen := "", 0 for _, h := range hints { if len(h.kw) > bestLen && strings.Contains(s, h.kw) { best, bestLen = h.val, len(h.kw) } } return best } // matchNameHint 是地名专用的入口:先掐掉括号里的地址再匹配。 func matchNameHint(hints []actHint, s string) string { return matchHint(hints, trimParens(s)) } // guessActivityByName 从地名猜活动,命中表示置信度高。 func guessActivityByName(name string) string { return matchNameHint(nameActHints, name) } // matchAmapType 在高德类型串里找映射。高德给的是「大类;小类」, // **小类更具体**(「风景名胜;公园」该按公园算,不是风景名胜),所以先试小类再退回大类。 func matchAmapType(hints []actHint, t string) string { if t == "" { return "" } if i := strings.LastIndexByte(t, ';'); i >= 0 { if v := matchHint(hints, t[i+1:]); v != "" { return v } return matchHint(hints, t[:i]) } return matchHint(hints, t) } func guessActivityFromAmapType(t string) string { return matchAmapType(amapActHints, t) } func guessCategoryFromAmapType(t string) string { return matchAmapType(mkHints, t) } // knownMKCategories 是真机确认用过的类别,界面上标一下,方便一眼看出哪些是「新猜的」。 var knownMKCategories = map[string]bool{ "MKPOICategoryRestaurant": true, "MKPOICategoryStore": true, "MKPOICategoryPublicTransport": true, "MKPOICategoryHotel": true, "MKPOICategoryLandmark": true, "MKPOICategoryFoodMarket": true, "MKPOICategoryBeach": true, "MKPOICategoryMuseum": true, "MKPOICategoryCampground": true, "MKPOICategoryZoo": true, "MKPOICategoryStadium": true, "MKPOICategoryPark": true, } // mkCategoryKnown 报告这个类别真机是否用过。 func mkCategoryKnown(c string) bool { return knownMKCategories[c] }