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

134 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package domain
import (
"sort"
"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",
}
type regionAliasEntry struct {
runes []rune
code string
}
// sortedRegionAliases is built once at startup: longest alias first so 「台北市」
// wins over 「台北」, with the alias itself breaking ties so detection order is
// reproducible instead of following map iteration.
var sortedRegionAliases = buildSortedRegionAliases()
func buildSortedRegionAliases() []regionAliasEntry {
out := make([]regionAliasEntry, 0, len(regionAliases))
for alias, code := range regionAliases {
if alias == "" {
continue
}
out = append(out, regionAliasEntry{runes: []rune(strings.ToLower(alias)), code: code})
}
sort.Slice(out, func(i, j int) bool {
if len(out[i].runes) != len(out[j].runes) {
return len(out[i].runes) > len(out[j].runes)
}
return string(out[i].runes) < string(out[j].runes)
})
return out
}
// DetectRegionCodes finds service-area codes mentioned in free text (exact alias only).
func DetectRegionCodes(text string) []string {
lower := strings.ToLower(text)
runes := []rune(lower)
if len(runes) == 0 {
return nil
}
seen := map[string]bool{}
var out []string
covered := make([]bool, len(runes))
for _, p := range sortedRegionAliases {
ar := p.runes
if len(ar) > len(runes) {
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 tokensmap 迭代無序,排序後再併入以維持輸出穩定
direct := make([]string, 0)
for code := range serviceAreaCodes {
if strings.Contains(lower, strings.ToLower(code)) && !seen[code] {
seen[code] = true
direct = append(direct, code)
}
}
sort.Strings(direct)
return append(out, direct...)
}
// 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
}