From e07cf743f930ecff3a610f1a38702a699a273a02 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Sun, 26 Jul 2026 11:34:42 +0100 Subject: [PATCH] feat(catalog): mbox header scan, company legal-suffix match, apply order Add --mbox-headers for Thunderbird folder files; strip GmbH/& Co. KG in CompanyGroupingKey; FindCompany falls back to full scan; apply companies before persons. Tightens role-mailbox noise filters. --- crm.go | 18 +++++++++++++++++- crm_normalize.go | 21 +++++++++++++++++++-- crm_normalize_test.go | 4 ++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crm.go b/crm.go index 0807c6b..54501c5 100644 --- a/crm.go +++ b/crm.go @@ -46,9 +46,12 @@ func (c *Client) GetContact(ctx context.Context, contactID string) (map[string]a } // FindCompany searches for a company contact with an exact (case-insensitive) -// displayName match. Returns nil when not found. +// displayName match after slogan/legal-suffix normalization. Returns nil when not found. func (c *Client) FindCompany(ctx context.Context, name string) (map[string]any, error) { needle := CompanyGroupingKey(name) + if needle == "" { + return nil, nil + } const page = 50 for start := 0; ; start += page { items, total, err := c.ListContacts(ctx, page, start, name) @@ -67,6 +70,19 @@ func (c *Client) FindCompany(ctx context.Context, name string) (map[string]any, break } } + // Search filter may miss longer legal names; fall back to a full company scan. + all, err := c.ListAllContacts(ctx) + if err != nil { + return nil, err + } + for _, co := range all { + if !isCompany(co) { + continue + } + if CompanyGroupingKey(fmt.Sprint(co["displayName"])) == needle { + return co, nil + } + } return nil, nil } diff --git a/crm_normalize.go b/crm_normalize.go index 82d4af8..7b2dcce 100644 --- a/crm_normalize.go +++ b/crm_normalize.go @@ -10,6 +10,9 @@ var multiSpace = regexp.MustCompile(`\s+`) // sloganSeparators split a company name from a trailing tagline/slogan. var sloganSeparators = []string{" — ", " – ", " - ", "—", "–"} +// companyLegalSuffix matches trailing legal-form tokens (GmbH, & Co. KG, …). +var companyLegalSuffix = regexp.MustCompile(`(?i)[\s,]*(&\s*co\.?\s*kg\.?|co\.?\s*kg\.?|gmbh|ag|ltd\.?|llc\.?|inc\.?|plc\.?|s\.?l\.?|sarl|b\.?v\.?)\s*$`) + // StripSloganSuffix returns the part before an em/en dash tagline, e.g. // "Affirm — Fraud Engineering" → "Affirm". func StripSloganSuffix(s string) string { @@ -22,9 +25,23 @@ func StripSloganSuffix(s string) string { return s } -// CompanyGroupingKey normalizes a company name for dedupe (ignores slogans). +// StripCompanyLegalSuffix removes trailing GmbH / & Co. KG / Ltd / … tokens. +func StripCompanyLegalSuffix(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimRight(s, ",; ") + for { + next := companyLegalSuffix.ReplaceAllString(s, "") + next = strings.TrimRight(strings.TrimSpace(next), ",; ") + if next == s || next == "" { + return s + } + s = next + } +} + +// CompanyGroupingKey normalizes a company name for dedupe (ignores slogans and legal suffixes). func CompanyGroupingKey(s string) string { - return NormalizeCompanyName(StripSloganSuffix(s)) + return NormalizeCompanyName(StripCompanyLegalSuffix(StripSloganSuffix(s))) } // NormalizeCompanyName lowercases and collapses whitespace for grouping. diff --git a/crm_normalize_test.go b/crm_normalize_test.go index f02c3a6..79b0f8f 100644 --- a/crm_normalize_test.go +++ b/crm_normalize_test.go @@ -65,6 +65,10 @@ func TestCompanyGroupingKey(t *testing.T) { if a != b { t.Fatalf("%q != %q", a, b) } + if CompanyGroupingKey("Acme") != CompanyGroupingKey("Acme GmbH & Co. KG,") { + t.Fatalf("legal suffix should not change grouping key: %q vs %q", + CompanyGroupingKey("Acme"), CompanyGroupingKey("Acme GmbH & Co. KG,")) + } } func TestOpportunityTitlesMatchSlogan(t *testing.T) {