| """Helpers for identifying official immigration source domains.""" | |
| from __future__ import annotations | |
| from urllib.parse import urlparse | |
| OFFICIAL_DOMAIN_HINTS: dict[str, list[str]] = { | |
| "AU": ["homeaffairs.gov.au", "immi.homeaffairs.gov.au"], | |
| "CA": ["canada.ca", "cic.gc.ca"], | |
| "DE": ["make-it-in-germany.com", "bamf.de", "auswaertiges-amt.de"], | |
| "DK": ["nyidanmark.dk"], | |
| "ES": ["inclusion.gob.es", "exteriores.gob.es"], | |
| "FI": ["migri.fi"], | |
| "FR": ["france-visas.gouv.fr", "service-public.fr"], | |
| "GB": ["gov.uk"], | |
| "IE": ["irishimmigration.ie", "enterprise.gov.ie"], | |
| "JP": ["isa.go.jp", "mofa.go.jp"], | |
| "NL": ["ind.nl"], | |
| "NO": ["udi.no"], | |
| "NZ": ["immigration.govt.nz"], | |
| "PT": ["aima.gov.pt", "eportugal.gov.pt", "vistos.mne.gov.pt"], | |
| "SE": ["migrationsverket.se"], | |
| "SG": ["ica.gov.sg", "mom.gov.sg"], | |
| "US": ["uscis.gov", "travel.state.gov"], | |
| } | |
| COUNTRY_ALIASES: dict[str, str] = { | |
| "australia": "AU", | |
| "canada": "CA", | |
| "denmark": "DK", | |
| "finland": "FI", | |
| "france": "FR", | |
| "germany": "DE", | |
| "ireland": "IE", | |
| "japan": "JP", | |
| "netherlands": "NL", | |
| "new zealand": "NZ", | |
| "norway": "NO", | |
| "portugal": "PT", | |
| "singapore": "SG", | |
| "spain": "ES", | |
| "sweden": "SE", | |
| "uk": "GB", | |
| "united kingdom": "GB", | |
| "united states": "US", | |
| "usa": "US", | |
| } | |
| UNOFFICIAL_CONTEXT_TERMS = ( | |
| "blog", | |
| "forum", | |
| "reddit", | |
| "quora", | |
| "lawfirm", | |
| "law-firm", | |
| "immigrationlaw", | |
| "relocation", | |
| "movingto", | |
| ) | |
| def domain_from_url(url: str) -> str: | |
| parsed = urlparse(url or "") | |
| host = parsed.netloc.lower().split("@")[-1].split(":")[0] | |
| if host.startswith("www."): | |
| host = host[4:] | |
| return host | |
| def is_domain_match(domain: str, official_domain: str) -> bool: | |
| normalized = official_domain.lower() | |
| return domain == normalized or domain.endswith(f".{normalized}") | |
| def hints_for_country(country: str | None) -> list[str]: | |
| if not country: | |
| return [] | |
| normalized = country.strip().upper() | |
| return OFFICIAL_DOMAIN_HINTS.get(normalized, []) | |
| def infer_country_codes(text: str, country: str | None = None) -> list[str]: | |
| codes: list[str] = [] | |
| if country: | |
| normalized = country.strip().upper() | |
| if normalized in OFFICIAL_DOMAIN_HINTS: | |
| codes.append(normalized) | |
| lowered = (text or "").lower() | |
| for alias, code in COUNTRY_ALIASES.items(): | |
| if alias in lowered and code not in codes: | |
| codes.append(code) | |
| return codes | |
| def infer_official_domains( | |
| text: str, | |
| country: str | None = None, | |
| *, | |
| max_domains: int = 8, | |
| ) -> list[str]: | |
| domains: list[str] = [] | |
| for code in infer_country_codes(text, country): | |
| for domain in OFFICIAL_DOMAIN_HINTS.get(code, []): | |
| if domain not in domains: | |
| domains.append(domain) | |
| if len(domains) >= max_domains: | |
| return domains | |
| return domains | |
| def classify_source(url: str) -> dict[str, object]: | |
| domain = domain_from_url(url) | |
| known_official = [ | |
| official_domain | |
| for domains in OFFICIAL_DOMAIN_HINTS.values() | |
| for official_domain in domains | |
| if is_domain_match(domain, official_domain) | |
| ] | |
| if known_official: | |
| return { | |
| "domain": domain, | |
| "type": "official_government", | |
| "is_official": True, | |
| "reason": f"Matches official domain hint {known_official[0]}", | |
| } | |
| if domain.endswith(".gov") or ".gov." in domain or domain.endswith(".gouv.fr"): | |
| return { | |
| "domain": domain, | |
| "type": "government", | |
| "is_official": True, | |
| "reason": "Government domain pattern", | |
| } | |
| if "embassy" in domain or "consulate" in domain: | |
| return { | |
| "domain": domain, | |
| "type": "embassy_or_consulate", | |
| "is_official": True, | |
| "reason": "Embassy or consulate domain pattern", | |
| } | |
| if any(term in domain for term in UNOFFICIAL_CONTEXT_TERMS): | |
| return { | |
| "domain": domain, | |
| "type": "unofficial_context", | |
| "is_official": False, | |
| "reason": "Likely blog, forum, legal-service, or relocation context", | |
| } | |
| return { | |
| "domain": domain, | |
| "type": "unknown", | |
| "is_official": False, | |
| "reason": "No official-domain signal detected", | |
| } | |