package discovery import ( "sort" "strings" "unicode" ) func normalizeDiscoverySearchText(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 discoveryQueryTokens(query string) []string { n := normalizeDiscoverySearchText(query) if n == "" { return nil } return strings.Fields(n) } func discoveryFieldMatchScore(haystack, needle string) float64 { h := normalizeDiscoverySearchText(haystack) n := normalizeDiscoverySearchText(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 appendNonEmptyFields(fields []string, values ...string) []string { for _, v := range values { if strings.TrimSpace(v) != "" { fields = append(fields, v) } } return fields } func profileSearchFields(p Profile) []string { fields := appendNonEmptyFields(nil, p.DisplayName, p.PrimaryEmail) if p.EnrichedData != nil { d := p.EnrichedData fields = appendNonEmptyFields(fields, d.FirstName, d.LastName, d.Company, d.Department, d.JobTitle, d.Website, d.Notes, ) for _, e := range d.Emails { fields = appendNonEmptyFields(fields, e.Value) } for _, ph := range d.Phones { fields = appendNonEmptyFields(fields, ph.Value) } for _, a := range d.Addresses { fields = appendNonEmptyFields(fields, a.Street, a.City, a.Region, a.PostalCode, a.Country) } } for _, e := range p.AllEmails { fields = appendNonEmptyFields(fields, e.Email, e.DisplayName) } for _, a := range p.DetectedInAccounts { fields = appendNonEmptyFields(fields, a.AccountEmail, a.AccountName) } return fields } func profileGroupSearchFields(group ProfileGroup) []string { fields := appendNonEmptyFields(nil, group.DisplayName, group.PrimaryEmail) for _, p := range group.Profiles { fields = append(fields, profileSearchFields(p)...) } if len(group.Profiles) == 0 { fields = append(fields, profileSearchFields(group.Profile)...) } seen := make(map[string]struct{}, len(fields)) unique := make([]string, 0, len(fields)) for _, f := range fields { key := normalizeDiscoverySearchText(f) if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} unique = append(unique, f) } return unique } func profileGroupQueryMatchScore(group ProfileGroup, query string) float64 { fields := profileGroupSearchFields(group) tokens := discoveryQueryTokens(query) if len(tokens) == 0 { return 0 } var total float64 for _, token := range tokens { best := 0.0 for _, field := range fields { best = max(best, discoveryFieldMatchScore(field, token)) best = max(best, discoveryFieldMatchScore(field, query)) } if best == 0 { return 0 } total += best } return total / float64(len(tokens)) } func rankProfileGroupsByQuery(groups []ProfileGroup, query string) []ProfileGroup { query = strings.TrimSpace(query) if query == "" { return groups } type scored struct { group ProfileGroup score float64 } matches := make([]scored, 0, len(groups)) for _, g := range groups { if score := profileGroupQueryMatchScore(g, query); score > 0 { matches = append(matches, scored{group: g, 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].group.DisplayName) < strings.ToLower(matches[j].group.DisplayName) }) out := make([]ProfileGroup, len(matches)) for i, m := range matches { out[i] = m.group } return out }