Google Maps Business Data in Python: How to Normalize Public Records for CRM Import
TL;DR: This article shows sales operations, RevOps, and Python developers how to take raw public business records from Google Maps and normalize them into a consistent JSON structure that is safe to import into a CRM. We use representative data, a runnable Python cleaning pipeline, and field-level mapping rules. The workflow is based on repositories maintained by scrapapi, not on a live production data service.
The Problem: Raw Public Records Are Messy
Public business listings are useful for lead research, but the JSON you collect is rarely CRM-ready. A single Google Maps record can carry the business name in name, title, or business_name; the phone in phone, phone_number, or telephone; and the address as a single string, a structured object, or a list of fragments.
If you import that data directly into a CRM, you end up with:
- Duplicate companies created because names are spelled differently.
- Missing phone numbers because the field name varies.
- Addresses split incorrectly, breaking territory reports.
- PII or private notes mixed into public fields.
- Compliance questions because you cannot prove where a record came from.
This guide is for RevOps teams, sales operations, and Python developers who need a repeatable normalization step before any CRM import. The goal is not to fetch live data, but to transform representative public business records into a clean, auditable format.
What This Workflow Does
The scrapapi organization publishes example repositories that demonstrate how to work with public business data in Python. For this article we focus on three:
- google-maps-shangjia-caiji — example patterns for handling public Google Maps business records.
- python-google-maps-data — reusable Python helpers for parsing, normalizing, and validating business records.
- crm-qiye-xinxi-buquan — field-mapping rules for enriching CRM company records with public business fields.
Together they show a workflow that:
- Accepts raw JSON records from a public data source.
- Normalizes names, phones, addresses, websites, and categories.
- Maps public fields to CRM company fields without overwriting private data.
- Adds provenance metadata so the record stays traceable.
- Outputs a JSON or JSONL file ready for CRM import.
Important: These repositories contain example data-processing code. They are not production APIs and do not guarantee live-data coverage, rate limits, or platform compliance. Any production workflow requires a verified data source, authentication, observability, retries, and a data-governance review.
Setup
You only need Python 3.10 or newer. Create a virtual environment and a working directory:
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install pydantic
The example below uses Pydantic for validation, but you can replace it with plain dictionaries if you prefer.
A Runnable Normalization Pipeline
The following Python script reads representative raw records, normalizes them, and writes CRM-ready JSON. It does not call any live API.
import json
import re
from typing import Any
from pydantic import BaseModel, Field, field_validator
class NormalizedBusiness(BaseModel):
"""A CRM-ready public business record."""
source_id: str
name: str = Field(..., min_length=1)
phone: str | None = None
website: str | None = None
address: str | None = None
city: str | None = None
country: str | None = "US"
categories: list[str] = Field(default_factory=list)
raw_source: str = "google-maps-public-record"
ingestion_date: str = "2026-08-13"
@field_validator("phone")
@classmethod
def clean_phone(cls, value: str | None) -> str | None:
if not value:
return None
digits = re.sub(r"\D", "", value)
if len(digits) == 10:
return f"+1-{digits[:3]}-{digits[3:6]}-{digits[6:]}"
if len(digits) == 11 and digits.startswith("1"):
return f"+1-{digits[1:4]}-{digits[4:7]}-{digits[7:]}"
return value
def normalize_record(raw: dict[str, Any]) -> NormalizedBusiness:
"""Convert a raw public record into a normalized form."""
# Accept multiple possible raw field names
name = raw.get("name") or raw.get("title") or raw.get("business_name") or ""
phone = raw.get("phone") or raw.get("phone_number") or raw.get("telephone")
website = raw.get("website") or raw.get("website_url") or raw.get("url")
# Address can be a string or a structured object
address_value = raw.get("address") or raw.get("formatted_address") or ""
if isinstance(address_value, dict):
address_parts = [
address_value.get("street"),
address_value.get("city"),
address_value.get("state"),
address_value.get("postal_code"),
]
address = ", ".join(part for part in address_parts if part)
city = address_value.get("city")
else:
address = str(address_value)
city = None
# Categories can be a list or a single pipe-delimited string
raw_categories = raw.get("categories") or raw.get("category") or []
if isinstance(raw_categories, str):
categories = [c.strip() for c in raw_categories.split("|") if c.strip()]
else:
categories = [str(c).strip() for c in raw_categories if c]
return NormalizedBusiness(
source_id=str(raw.get("place_id") or raw.get("id") or ""),
name=name.strip(),
phone=phone,
website=website,
address=address,
city=city,
categories=categories,
)
def main():
raw_records = [
{
"place_id": "gm_001",
"name": "Downtown Coffee Roasters",
"phone": "(555) 123-4567",
"website": "https://downtowncoffee.example.com",
"address": "1200 Market St, San Francisco, CA 94102",
"categories": ["Coffee shop", "Cafe"],
},
{
"id": "gm_002",
"title": "Bayview Bakery",
"phone_number": "5559876543",
"url": "http://bayviewbakery.example.com",
"formatted_address": {
"street": "4500 3rd St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94158",
},
"category": "Bakery | Wholesale bakery",
},
]
normalized = [normalize_record(record).model_dump() for record in raw_records]
with open("crm_ready_businesses.json", "w", encoding="utf-8") as f:
json.dump(normalized, f, indent=2, ensure_ascii=False)
print(json.dumps(normalized, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
When you run the script, it produces two normalized records:
[
{
"source_id": "gm_001",
"name": "Downtown Coffee Roasters",
"phone": "+1-555-123-4567",
"website": "https://downtowncoffee.example.com",
"address": "1200 Market St, San Francisco, CA 94102",
"city": null,
"country": "US",
"categories": ["Coffee shop", "Cafe"],
"raw_source": "google-maps-public-record",
"ingestion_date": "2026-08-13"
},
{
"source_id": "gm_002",
"name": "Bayview Bakery",
"phone": "+1-555-987-6543",
"website": "http://bayviewbakery.example.com",
"address": "4500 3rd St, San Francisco, CA 94158",
"city": "San Francisco",
"country": "US",
"categories": ["Bakery", "Wholesale bakery"],
"raw_source": "google-maps-public-record",
"ingestion_date": "2026-08-13"
}
]
This output is representative. Your real input will have different field names, different address formats, and different category schemes. The important part is the normalization layer, not the sample data.
Field Mapping Without Overwriting Private CRM Data
A common mistake is to let public data overwrite fields that your sales team has already researched. Instead, map public fields into separate enrichment fields and let the CRM owner decide when to promote them.
| Raw public field | Normalized field | CRM target field | Overwrite rule |
|---|---|---|---|
name, title
|
name |
Company Name |
Only if CRM name is blank |
phone, phone_number
|
phone |
Public Phone |
Add if missing; do not replace verified phone |
website, url
|
website |
Company Website |
Only if blank or flagged as outdated |
address, formatted_address
|
address |
Public Address |
Add as secondary address |
categories, category
|
categories |
Industry Tags |
Merge, deduplicate |
The crm-qiye-xinxi-buquan repository explores this mapping in more detail, including rules for handling Chinese-language company records and privacy-sensitive fields.
Use Cases
- Local lead generation: Normalize a list of businesses in a target neighborhood before loading them into HubSpot, Salesforce, or Pipedrive.
- Territory planning: Extract city, state, and postal code from address strings so the CRM can assign records to the right sales rep.
- Deduplication prep: Standardize phone numbers and websites so matching algorithms can identify duplicate companies.
- AI agent context: Provide a clean JSON record that an LLM-based agent can reason about without being exposed to raw, inconsistent source data.
Compliance and Limitations Checklist
Before importing any public data into a CRM, verify the following:
- [ ] The data comes from a public source and does not include private or scraped-from-private-source records.
- [ ] You have reviewed the target platform's terms of service and robots policy.
- [ ] You respect applicable privacy laws for the regions where the businesses and your CRM users are located.
- [ ] You keep provenance metadata so every record is traceable to its source and ingestion date.
- [ ] You do not claim coverage, accuracy, or freshness that you cannot verify.
- [ ] You separate public enrichment fields from privately researched fields to avoid overwriting sales intelligence.
- [ ] You have retries, observability, and rate-limit handling if your production pipeline calls an external service.
This article and the referenced repositories focus only on public web data and public business information. They do not cover hacking, account cracking, private data, stolen databases, CAPTCHA bypass, or evading access controls.
Frequently Asked Questions
Q1: Does this code fetch live Google Maps data?
No. The Python script above processes representative JSON records stored in the file. It does not call any live API or unverified endpoint. A production workflow needs a verified data source and proper authentication.
Q2: Can I import the output directly into HubSpot or Salesforce?
The JSON structure is designed to be import-friendly, but you should still map the fields to your CRM's import format. Always test with a small batch first and validate phone numbers and addresses.
Q3: How do I handle records with missing phones or addresses?
Keep the record and mark the missing fields as null. Do not invent data. You can add a data_quality_score field to flag records that need manual review.
Q4: Should I overwrite existing CRM company names with public data?
Generally no. Public data should land in enrichment fields. Let the sales or RevOps team decide when to promote a public value to the primary field.
Q5: What if the raw record uses a language other than English?
The normalization logic is language-agnostic for structure, but you may need locale-specific rules for phone numbers, address order, and category translation. The crm-qiye-xinxi-buquan repository includes examples for Chinese-language company records.
Q6: How do I prove where a record came from during an audit?
Store source_id, raw_source, and ingestion_date in the normalized record and, if possible, in the CRM itself. Provenance metadata is the simplest way to make public data auditable.
Q7: Where can I find more example workflows?
Browse the scrapapi GitHub profile. The python-google-maps-data repository has additional parsing utilities, and google-maps-shangjia-caiji shows example patterns for handling Google Maps business records.
Start Normalizing Public Business Records
If you are building a RevOps or sales-operations workflow around public business data, the repositories maintained by scrapapi give you a starting point:
- google-maps-shangjia-caiji for example Google Maps record patterns.
- python-google-maps-data for reusable Python normalization helpers.
- crm-qiye-xinxi-buquan for CRM field-mapping and enrichment rules.
Remember to treat these as example data-processing tools, not production data sources. For production, add authentication, observability, retries, rate-limit handling, and a data-governance review.
Top comments (0)