package contacts import ( "sort" "strings" "unicode" "github.com/ultisuite/ulti-backend/internal/nextcloud" ) func normalizeContactSearchText(value string) string { var b strings.Builder b.Grow(len(value)) for _, r := range strings.ToLower(strings.TrimSpace(value)) { if unicode.IsMark(r) { continue } b.WriteRune(r) } return b.String() } func queryTokens(query string) []string { n := normalizeContactSearchText(query) if n == "" { return nil } return strings.Fields(n) } func fieldMatchScore(haystack, needle string) float64 { h := normalizeContactSearchText(haystack) n := normalizeContactSearchText(needle) if n == "" || !strings.Contains(h, n) { return 0 } if h == n { return 1 } if strings.HasPrefix(h, n) { return 0.95 + 0.05*float64(len(n))/float64(max(len(h), 1)) } for _, word := range strings.FieldsFunc(h, func(r rune) bool { return r == ' ' || r == '@' || r == '.' || r == '_' || r == '+' || r == '-' }) { if word == "" { continue } if strings.HasPrefix(word, n) { return 0.88 + 0.07*float64(len(n))/float64(max(len(word), 1)) } } idx := strings.Index(h, n) positionBonus := 1 - float64(idx)/float64(max(len(h), 1))*0.35 lengthBonus := float64(len(n)) / float64(max(len(h), 1)) return 0.42 + 0.28*positionBonus + 0.22*lengthBonus } func contactSearchFields(c nextcloud.Contact) []string { out := make([]string, 0, 4) for _, v := range []string{c.FullName, c.Email, c.Phone, c.Org} { if strings.TrimSpace(v) != "" { out = append(out, v) } } return out } func contactQueryMatchScore(c nextcloud.Contact, query string) float64 { fields := contactSearchFields(c) tokens := queryTokens(query) if len(tokens) == 0 { return 0 } var total float64 for _, token := range tokens { best := 0.0 for _, field := range fields { best = max(best, fieldMatchScore(field, token)) best = max(best, fieldMatchScore(field, query)) } if best == 0 { return 0 } total += best } return total / float64(len(tokens)) } func rankContactsByQuery(contacts []nextcloud.Contact, query string) []nextcloud.Contact { query = strings.TrimSpace(query) if query == "" { return contacts } type scored struct { contact nextcloud.Contact score float64 } matches := make([]scored, 0, len(contacts)) for _, c := range contacts { if score := contactQueryMatchScore(c, query); score > 0 { matches = append(matches, scored{contact: c, score: score}) } } sort.SliceStable(matches, func(i, j int) bool { if matches[i].score != matches[j].score { return matches[i].score > matches[j].score } return strings.ToLower(matches[i].contact.FullName) < strings.ToLower(matches[j].contact.FullName) }) out := make([]nextcloud.Contact, len(matches)) for i, m := range matches { out[i] = m.contact } return out }