117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
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
|
|
}
|