Business Matching Guidance
When trying to match to business page on Yelp, we suggest normalizing your data to best match how we represent the data on Yelp.com to increase the likelihood that we will find and return the desired match. This includes potentially reformatting the business name and the address to align with our Data Quality Guidelines.
How to Match a Business to a Yelp Business ID
When integrating with Yelp's platform, the first step is often mapping your internal business records to Yelp's canonical business_id. This guide covers a multi-strategy approach that maximizes match rates while maintaining high confidence.
Overview
Yelp offers three endpoints that can help identify a business:
| Endpoint | Best When You Have | Typical Match Rate |
|---|---|---|
| Business Match | Name + full address | Highest precision |
| Phone Search | A verified phone number | High precision with validation |
| Business Search | Name + city/state | Broadest recall |
No single endpoint covers every case. Businesses may have address variations, outdated phone numbers, or name differences between your records and Yelp's. By combining all three strategies and scoring the results, you can achieve match rates well above what any single method provides.
Strategy 1: Business Match API
The /v3/businesses/matches endpoint is purpose-built for this use case. It takes structured business details and returns the best match.
When it works best: You have accurate name and address data.
When it may miss: Address formatting differences, suite/unit variations, or businesses that have moved.
def business_match(name, address1, city, state, country, address2=None, zip_code=None):
params = {
'name': name,
'city': city,
'state': state,
'country': country,
}
if address1:
params['address1'] = address1
if address2:
params['address2'] = address2
if zip_code:
params['zip_code'] = zip_code
response = requests.get(
'https://api.yelp.com/v3/businesses/matches',
headers={'Authorization': f'Bearer {API_KEY}'},
params=params
)
businesses = response.json().get('businesses', [])
return businesses[0] if businesses else None
Strategy 2: Phone Search
The /v3/businesses/search/phone endpoint finds businesses by phone number. This is powerful because phone numbers are highly unique but requires validation since phone numbers can be reassigned.
When it works best: You have a reliable phone number for the business.
When it may miss: VoIP numbers, call centers, or numbers shared across franchise locations.
Important: Always validate that the returned business is in the expected location. A phone number match in the wrong city likely indicates a reassigned number or a data error.
def phone_search(phone, expected_city=None, expected_state=None):
# Normalize to E.164 format
digits = ''.join(filter(str.isdigit, phone))
if len(digits) == 10:
formatted = f"+1{digits}"
elif len(digits) == 11 and digits.startswith('1'):
formatted = f"+{digits}"
else:
formatted = f"+1{digits}"
response = requests.get(
'https://api.yelp.com/v3/businesses/search/phone',
headers={'Authorization': f'Bearer {API_KEY}'},
params={'phone': formatted}
)
businesses = response.json().get('businesses', [])
if not businesses:
return None
result = businesses[0]
# Validate location if expected city/state provided
if expected_city or expected_state:
location = result.get('location', {})
city_match = (not expected_city or
location.get('city', '').lower() == expected_city.lower())
state_match = (not expected_state or
location.get('state', '').lower() == expected_state.lower())
if not (city_match or state_match):
return None # Wrong location, reject
return result
Strategy 3: Business Search with Fuzzy Matching
The /v3/businesses/search endpoint is the broadest net. Search by business name in a location, then fuzzy-match the results to find the best candidate.
When it works best: As a fallback when the other two methods miss, or when address data is incomplete.
When it may miss: Very common business names in dense areas, or businesses with significantly different names on Yelp.
def city_state_search(name, city, state):
response = requests.get(
'https://api.yelp.com/v3/businesses/search',
headers={'Authorization': f'Bearer {API_KEY}'},
params={
'term': name,
'location': f"{city}, {state}",
'limit': 50
}
)
businesses = response.json().get('businesses', [])
return find_best_fuzzy_match(name, businesses)
Confidence Scoring
Raw API matches aren't always correct. Use fuzzy name comparison to score confidence and reject false positives.
Name Similarity
Compare input and result names using multiple similarity metrics and take the best:
- Exact ratio character-by-character similarity
- Token sort ratio ignores word order ("Joe's Pizza" vs "Pizza by Joe's")
- Partial ratio handles substring matches (with length-penalty for very different lengths)
Removing Generic Terms
Industry-generic terms (e.g., "Salon," "Fitness," "Restaurant") inflate similarity scores. Strip these before scoring so the comparison focuses on the distinctive part of the name.
GENERIC_TERMS = ['salon', 'spa', 'studio', 'fitness', 'restaurant', 'bar', 'grill']
def remove_generic_terms(name):
cleaned = name.lower()
for term in GENERIC_TERMS:
cleaned = re.sub(r'\b' + re.escape(term) + r'\b', '', cleaned)
return ' '.join(cleaned.split()).strip()
Recommended Thresholds
| Threshold | Purpose |
|---|---|
| 70 | Minimum name score: reject matches below this |
| 75 | Confidence threshold for fuzzy search results |
| 80 | High confidence: safer for automated processing |
Choosing the Best Match
When multiple strategies return results, pick the one with the highest confidence score. If two methods return the same business_id, that's a strong signal the match is correct regardless of individual scores.
def choose_best_match(biz_match, phone_match, search_match):
candidates = []
for match, method in [(biz_match, 'business_match'),
(phone_match, 'phone_search'),
(search_match, 'city_state_search')]:
if match and match.get('score'):
candidates.append({**match, 'method': method})
if not candidates:
return None
candidates.sort(key=lambda x: x['score'], reverse=True)
return candidates[0]
Putting It All Together
The reference implementation below combines all three strategies into a single function you can call for each business in your dataset. See the full reference script for a complete, runnable example.
def match_business(name, address, city, state, country='US',
address2=None, zip_code=None, phone=None):
"""
Attempt to match a business to a Yelp business_id using three strategies.
Returns the best match with confidence score and method used.
"""
# Strategy 1: Business Match API
biz_result = try_business_match(name, address, address2, city, state, zip_code, country)
# Strategy 2: Phone Search (with location validation)
phone_result = try_phone_search(phone, name, city, state) if phone else None
# Strategy 3: City+State fuzzy search
search_result = try_city_state_search(name, city, state)
# Pick the best
return choose_best_match(biz_result, phone_result, search_result)
Tips for Production Use
- Rate limiting: All Yelp API endpoints have rate limits. Implement backoff on HTTP 429 responses and retry with exponential delay.
- Batch processing: When matching large datasets, run the three strategies concurrently per business but respect overall QPS limits.
- Normalize inputs: Standardize state names to 2-letter codes, phone numbers to E.164, and zip codes to 5 digits before calling the APIs.
- Log rejections: Track which businesses failed to match and why, this helps you tune thresholds and identify data quality issues in your source records.
- Country detection: If you don't have explicit country data, infer it from state/province codes (e.g.,
ON,BC,AB→ Canada).
Reference Implementation
Below is a complete Python script implementing the multi-strategy matching approach. It's designed to be imported and called programmatically, pass in business details and get back match results.
Full reference script:
"""
Yelp Business ID Matching: Reference Implementation
Matches a business record to a Yelp business_id using three complementary strategies:
1. Business Match API (structured address lookup)
2. Phone Search API (phone number lookup with location validation)
3. Business Search API (name + location fuzzy search)
Usage:
from business_id_matching_reference import match_business
result = match_business(
name="Joe's Coffee",
address="123 Main St",
city="San Francisco",
state="CA",
country="US",
phone="4155551234"
)
if result:
print(f"Matched: {result['business_id']} via {result['method']} "
f"(confidence: {result['confidence_score']})")
Requirements:
pip install requests rapidfuzz
"""
import re
import requests
from rapidfuzz import fuzz
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
API_KEY = 'YOUR_API_KEY_HERE'
YELP_MATCH_URL = 'https://api.yelp.com/v3/businesses/matches'
YELP_PHONE_SEARCH_URL = 'https://api.yelp.com/v3/businesses/search/phone'
YELP_SEARCH_URL = 'https://api.yelp.com/v3/businesses/search'
SEARCH_LIMIT = 50
CONFIDENCE_THRESHOLD = 75
MINIMUM_NAME_SCORE = 70
# Generic terms to strip before name comparison.
# Customize this list for your industry vertical.
GENERIC_TERMS = [
'inc', 'llc', 'dba', 'corp', 'corporation', 'ltd', 'company',
]
# US state/Canadian province abbreviation lookup
STATE_ABBREVIATIONS = {
'alabama': 'AL', 'alaska': 'AK', 'arizona': 'AZ', 'arkansas': 'AR',
'california': 'CA', 'colorado': 'CO', 'connecticut': 'CT', 'delaware': 'DE',
'florida': 'FL', 'georgia': 'GA', 'hawaii': 'HI', 'idaho': 'ID',
'illinois': 'IL', 'indiana': 'IN', 'iowa': 'IA', 'kansas': 'KS',
'kentucky': 'KY', 'louisiana': 'LA', 'maine': 'ME', 'maryland': 'MD',
'massachusetts': 'MA', 'michigan': 'MI', 'minnesota': 'MN', 'mississippi': 'MS',
'missouri': 'MO', 'montana': 'MT', 'nebraska': 'NE', 'nevada': 'NV',
'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY',
'north carolina': 'NC', 'north dakota': 'ND', 'ohio': 'OH', 'oklahoma': 'OK',
'oregon': 'OR', 'pennsylvania': 'PA', 'rhode island': 'RI', 'south carolina': 'SC',
'south dakota': 'SD', 'tennessee': 'TN', 'texas': 'TX', 'utah': 'UT',
'vermont': 'VT', 'virginia': 'VA', 'washington': 'WA', 'west virginia': 'WV',
'wisconsin': 'WI', 'wyoming': 'WY', 'district of columbia': 'DC',
'alberta': 'AB', 'british columbia': 'BC', 'manitoba': 'MB',
'new brunswick': 'NB', 'newfoundland and labrador': 'NL',
'northwest territories': 'NT', 'nova scotia': 'NS', 'nunavut': 'NU',
'ontario': 'ON', 'prince edward island': 'PE', 'quebec': 'QC',
'saskatchewan': 'SK', 'yukon': 'YT',
}
CANADIAN_PROVINCES = {
'AB', 'BC', 'MB', 'NB', 'NL', 'NT', 'NS', 'NU', 'ON', 'PE', 'QC', 'SK', 'YT'
}
# ---------------------------------------------------------------------------
# Input Normalization
# ---------------------------------------------------------------------------
def normalize_state(state):
"""Convert full state/province name to 2-letter abbreviation."""
if not state:
return ''
state = state.strip()
if len(state) == 2:
return state.upper()
return STATE_ABBREVIATIONS.get(state.lower(), state.upper())
def normalize_phone(phone):
"""Normalize phone number to E.164 format (+1XXXXXXXXXX)."""
if not phone:
return ''
digits = ''.join(filter(str.isdigit, phone))
if not digits:
return ''
if len(digits) == 10:
return f'+1{digits}'
if len(digits) == 11 and digits.startswith('1'):
return f'+{digits}'
return f'+1{digits}'
def normalize_zip(zip_code, country='US'):
"""Validate and normalize zip/postal code."""
if not zip_code:
return ''
zip_code = zip_code.strip()
if country == 'CA':
cleaned = re.sub(r'\s+', '', zip_code).upper()
if re.match(r'^[A-Z]\d[A-Z]\d[A-Z]\d$', cleaned):
return cleaned
return ''
digits = ''.join(filter(str.isdigit, zip_code))
return digits[:5] if len(digits) >= 5 else ''
def detect_country(state):
"""Infer country from state/province abbreviation."""
if state and state.upper() in CANADIAN_PROVINCES:
return 'CA'
return 'US'
# ---------------------------------------------------------------------------
# Name Scoring
# ---------------------------------------------------------------------------
def remove_generic_terms(name):
"""Strip industry-generic terms that inflate similarity scores."""
cleaned = name.lower()
for term in GENERIC_TERMS:
cleaned = re.sub(r'\b' + re.escape(term) + r'\b', '', cleaned)
return ' '.join(cleaned.split()).strip()
def calculate_name_score(input_name, yelp_name):
"""
Compute a confidence score (0-100) for how well two business names match.
Uses multiple fuzzy strategies and penalizes length mismatches.
"""
input_lower = input_name.lower().strip()
yelp_lower = yelp_name.lower().strip()
ratio = fuzz.ratio(input_lower, yelp_lower)
partial = fuzz.partial_ratio(input_lower, yelp_lower)
token_sort = fuzz.token_sort_ratio(input_lower, yelp_lower)
# Penalize partial_ratio when lengths are very different
len_ratio = min(len(input_lower), len(yelp_lower)) / max(len(input_lower), len(yelp_lower), 1)
if len_ratio < 0.6:
partial = partial * (0.4 + len_ratio * 0.6)
full_score = max(ratio, partial, token_sort)
# Disallow partial_ratio dominance when one name is much shorter
if full_score == partial and len_ratio < 0.5:
full_score = max(ratio, token_sort)
# Score with generic terms removed
input_cleaned = remove_generic_terms(input_name)
yelp_cleaned = remove_generic_terms(yelp_name)
if input_cleaned and yelp_cleaned:
cleaned_score = max(
fuzz.ratio(input_cleaned, yelp_cleaned),
fuzz.token_sort_ratio(input_cleaned, yelp_cleaned)
)
else:
cleaned_score = full_score * 0.5
# Blend: 60% cleaned (distinctive parts) + 40% full name
return round(cleaned_score * 0.6 + full_score * 0.4, 2)
# ---------------------------------------------------------------------------
# Location Validation
# ---------------------------------------------------------------------------
def check_location_match(input_city, input_state, input_zip, yelp_city, yelp_state, yelp_zip):
"""
Verify that a candidate match is in the expected location.
Returns True if city, state, or zip matches.
"""
if input_zip and yelp_zip:
input_digits = ''.join(filter(str.isdigit, str(input_zip)))[:5]
yelp_digits = ''.join(filter(str.isdigit, str(yelp_zip)))[:5]
if input_digits and input_digits == yelp_digits:
return True
if input_city and yelp_city:
if input_city.lower().strip() == yelp_city.lower().strip():
return True
if input_state and yelp_state:
if input_state.lower().strip() == yelp_state.lower().strip():
return True
return False
# ---------------------------------------------------------------------------
# API Calls
# ---------------------------------------------------------------------------
def _headers():
return {'Authorization': f'Bearer {API_KEY}'}
def _extract_info(business):
"""Extract standardized fields from a Yelp API business object."""
location = business.get('location', {})
return {
'business_id': business.get('id', ''),
'name': business.get('name', ''),
'address': ', '.join(location.get('display_address', [])),
'city': location.get('city', ''),
'state': location.get('state', ''),
'zip_code': location.get('zip_code', ''),
'phone': business.get('phone', ''),
}
def try_business_match(name, address, city, state, country, address2=None, zip_code=None):
"""
Strategy 1: Use the Business Match API for a structured lookup.
Returns match info dict with confidence_score, or None.
"""
params = {
'name': name,
'city': city,
'state': state,
'country': country,
}
if address:
params['address1'] = address
if address2:
params['address2'] = address2
if zip_code:
params['zip_code'] = zip_code
response = requests.get(YELP_MATCH_URL, headers=_headers(), params=params)
if response.status_code != 200:
return None
businesses = response.json().get('businesses', [])
if not businesses:
return None
info = _extract_info(businesses[0])
score = calculate_name_score(name, info['name'])
if score < MINIMUM_NAME_SCORE:
return None
return {**info, 'confidence_score': score, 'method': 'business_match'}
def try_phone_search(phone, name, city=None, state=None, zip_code=None):
"""
Strategy 2: Search by phone number, then validate location and name.
Returns match info dict with confidence_score, or None.
"""
if not phone:
return None
formatted = phone if phone.startswith('+') else f'+{phone}'
response = requests.get(
YELP_PHONE_SEARCH_URL,
headers=_headers(),
params={'phone': formatted}
)
if response.status_code != 200:
return None
businesses = response.json().get('businesses', [])
if not businesses:
return None
info = _extract_info(businesses[0])
# Validate location — reject if city/state/zip all mismatch
if city or state or zip_code:
if not check_location_match(city, state, zip_code,
info['city'], info['state'], info['zip_code']):
return None
# Validate name similarity
score = calculate_name_score(name, info['name'])
if score < MINIMUM_NAME_SCORE:
return None
return {**info, 'confidence_score': score, 'method': 'phone_search'}
def try_city_state_search(name, city, state):
"""
Strategy 3: General search by name + location, then fuzzy match results.
Returns match info dict with confidence_score, or None.
"""
response = requests.get(
YELP_SEARCH_URL,
headers=_headers(),
params={
'term': name,
'location': f'{city}, {state}',
'limit': SEARCH_LIMIT,
}
)
if response.status_code != 200:
return None
businesses = response.json().get('businesses', [])
if not businesses:
return None
best_match = None
best_score = 0
for biz in businesses:
yelp_name = biz.get('name', '')
score = calculate_name_score(name, yelp_name)
if score > best_score:
best_score = score
best_match = biz
if best_score < CONFIDENCE_THRESHOLD:
return None
info = _extract_info(best_match)
return {**info, 'confidence_score': best_score, 'method': 'city_state_search'}
# ---------------------------------------------------------------------------
# Main Matching Function
# ---------------------------------------------------------------------------
def match_business(name, city, state, address=None, address2=None,
zip_code=None, phone=None, country=None):
"""
Match a business to a Yelp business_id using all available strategies.
Args:
name: Business name (required)
city: City (required)
state: State or province, full name or 2-letter code (required)
address: Street address
address2: Suite, unit, floor, etc.
zip_code: ZIP or postal code
phone: Phone number (any format)
country: 2-letter country code (auto-detected from state if omitted)
Returns:
Dict with business_id, name, address, confidence_score, and method used.
None if no confident match was found.
"""
# Normalize inputs
state = normalize_state(state)
phone = normalize_phone(phone) if phone else ''
if not country:
country = detect_country(state)
zip_code = normalize_zip(zip_code, country) if zip_code else ''
# Run all three strategies
biz_result = try_business_match(name, address, city, state, country, address2, zip_code)
phone_result = try_phone_search(phone, name, city, state, zip_code) if phone else None
search_result = try_city_state_search(name, city, state)
# Collect successful matches and pick the highest confidence
candidates = [r for r in [biz_result, phone_result, search_result] if r]
if not candidates:
return None
candidates.sort(key=lambda x: x['confidence_score'], reverse=True)
return candidates[0]
# ---------------------------------------------------------------------------
# Example Usage
# ---------------------------------------------------------------------------
if __name__ == '__main__':
# Replace with a real business to test
result = match_business(
name="The Coffee Shop",
address="123 Main Street",
city="San Francisco",
state="California",
phone="(415) 555-1234",
)
if result:
print(f"Matched: {result['business_id']}")
print(f" Name: {result['name']}")
print(f" Address: {result['address']}")
print(f" Method: {result['method']}")
print(f" Confidence: {result['confidence_score']}")
else:
print("No confident match found.")
Here are some additional items to consider:
-
Special characters (such as @, #, /, and parentheses) and excessive store information in the business name and address fields, such as location tags, dba, and store numbers. These are typically not allowed per our Yelp listing guidelines and our system cannot match them. Examples:
JACK IN THE BOX #7310
ACCENT FOOD SERVICES @ KYND CANNABIS
TACO BELL #31858
GRILL AT QUAIL CORNERS (THE)
EGG ROLL KING @ S. WELLS AVE -
Legal entities in the business name field (we only list the DBA; businesses with Inc., LLC, Co, Group, etc. are probably causing the no matches). Examples:
UMCo LLC DBA THE URBAN MARKET
TACOS JALISCO CANTINA & GRILL LLC
BIBO COFFEE CO INC
MINDFUL CUPCAKES LLC
SFP DEVELOPMENT CO LLC/DBA MOD PIZZA -
Remove irrelevant businesses (i.e. non-restaurants/food businesses) and businesses we don't list on Yelp, such as the employee cafeteria at an office. Examples:
HARRAHS RENO EMPLOYEES LOUNGE (not eligible for Yelp)
ACCENT FOOD SERVICES CASHMAN UPSTAIRS BREAKROOM (not eligible for Yelp)
CVS PHARMACY #3948 (not a restaurant/food biz)
GOLDEN GATE PETROLEUM OF NEVADA LLC (not a restaurant/food biz)
DOLLAR TREE #6733 (not a restaurant/food biz)
FOOD ACCENT FOOD SERVICES SUMMIT RACING CALL CENTER (not a restaurant/food biz and also not eligible for Yelp)
ACCENT FOOD SERVICES SHERWIN WILLIAMS (this sounds like food services for employees as Sherwin Williams is a paint store, so it wouldn't be eligible since it's not consumer-facing)
WASHOE LITTLE LEAGUE (not a restaurant/food biz)
ELDORADO HOTEL EMPLOYEE RESTAURANT (not eligible for Yelp) -
Joint businesses (or businesses located inside another). We have separate Yelp listings for each entity, so these need to come through as separate businesses. Examples:
AFC SUSHI / HOT WOK @ RALEYS #105
HYATT REGENCY LAKE TAHOE/TAHOE PROVISIONS
TARGET STORE T1363/PIZZA HUT EXPRESS -
Businesses with no clear identity - concession stands at stadiums, amusement parks, and convention centers usually do not have a clear identity or distinctive business name and therefore, we do not grant them listings. Examples:
PEPPERMILL 2ND FLR CONVENTION SERVICE BAR
NUGGET SPARKS GAME ON SMALL BAR
LEVY PREMIUM FOOD SERVICE LTD CONCESSION STAND TWO
LEVY PREMIUM FOOD SERVICE THEME CART
ELDORADO HOTEL ROOM SERV BAR
ST JAMES INFIRMARY UPSTAIRS OUTSIDE BAR
Updated 20 days ago
