thread-master/apps/backend/internal/module/radar/domain/region.go

117 lines
3.8 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
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
}