package domain import "strings" // regionAliases maps common Taiwan place names (zh) → service area code. // Exact alias match only — never geo-infer (OP-04). var regionAliases = map[string]string{ "台北": "TPE", "臺北": "TPE", "台北市": "TPE", "臺北市": "TPE", "tpe": "TPE", "新北": "NWT", "新北市": "NWT", "nwt": "NWT", "桃園": "TAO", "桃園市": "TAO", "tao": "TAO", "台中": "TXG", "臺中": "TXG", "台中市": "TXG", "臺中市": "TXG", "txg": "TXG", "台南": "TNN", "臺南": "TNN", "台南市": "TNN", "臺南市": "TNN", "tnn": "TNN", "高雄": "KHH", "高雄市": "KHH", "khh": "KHH", "基隆": "KEE", "基隆市": "KEE", "kee": "KEE", "新竹市": "HSZ", "hsz": "HSZ", "新竹縣": "HSQ", "hsq": "HSQ", "新竹": "HSZ", "苗栗": "MIA", "苗栗縣": "MIA", "mia": "MIA", "彰化": "CHA", "彰化縣": "CHA", "cha": "CHA", "南投": "NAN", "南投縣": "NAN", "nan": "NAN", "雲林": "YUN", "雲林縣": "YUN", "yun": "YUN", "嘉義市": "CYI", "cyi": "CYI", "嘉義縣": "CYQ", "cyq": "CYQ", "嘉義": "CYI", "屏東": "PIF", "屏東縣": "PIF", "pif": "PIF", "宜蘭": "ILA", "宜蘭縣": "ILA", "ila": "ILA", "花蓮": "HUA", "花蓮縣": "HUA", "hua": "HUA", "台東": "TTT", "臺東": "TTT", "台東縣": "TTT", "臺東縣": "TTT", "ttt": "TTT", "澎湖": "PEN", "澎湖縣": "PEN", "pen": "PEN", "金門": "KIN", "金門縣": "KIN", "kin": "KIN", "連江": "LIE", "連江縣": "LIE", "馬祖": "LIE", "lie": "LIE", } // DetectRegionCodes finds service-area codes mentioned in free text (exact alias only). func DetectRegionCodes(text string) []string { lower := strings.ToLower(text) seen := map[string]bool{} var out []string // Longer aliases first so 「台北市」 wins over 「台北」. type pair struct{ alias, code string } pairs := make([]pair, 0, len(regionAliases)) for a, c := range regionAliases { pairs = append(pairs, pair{a, c}) } // crude length sort for i := 0; i < len(pairs); i++ { for j := i + 1; j < len(pairs); j++ { if len([]rune(pairs[j].alias)) > len([]rune(pairs[i].alias)) { pairs[i], pairs[j] = pairs[j], pairs[i] } } } matched := make([]string, len(lower)) copy(matched, []string{}) // silence unused if empty _ = matched covered := make([]bool, len([]rune(lower))) runes := []rune(lower) for _, p := range pairs { alias := strings.ToLower(p.alias) ar := []rune(alias) if len(ar) == 0 { continue } for i := 0; i+len(ar) <= len(runes); i++ { ok := true for k := 0; k < len(ar); k++ { if runes[i+k] != ar[k] || covered[i+k] { ok = false break } } if !ok { continue } for k := 0; k < len(ar); k++ { covered[i+k] = true } if !seen[p.code] { seen[p.code] = true out = append(out, p.code) } } } // also direct code tokens for code := range serviceAreaCodes { if strings.Contains(lower, strings.ToLower(code)) && !seen[code] { seen[code] = true out = append(out, code) } } return out } // MatchRegion compares detected codes against the owner's service areas. // unknown when nothing detected; mismatch when only non-overlapping codes; match on any overlap. func MatchRegion(detected, serviceAreas []string, remoteOK bool) (regionMatch string, score int) { if len(detected) == 0 { // partial credit for unknown (spec §3.3) return RegionUnknown, WeightRegion / 2 } if len(serviceAreas) == 0 { return RegionUnknown, WeightRegion / 2 } svc := map[string]bool{} for _, a := range serviceAreas { svc[strings.ToUpper(strings.TrimSpace(a))] = true } for _, d := range detected { if svc[strings.ToUpper(strings.TrimSpace(d))] { return RegionMatch, WeightRegion } } if remoteOK { // OP-05: remote_ok → mismatch not hard-reject; full region points return RegionMismatch, WeightRegion } return RegionMismatch, 0 }