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:

EndpointBest When You HaveTypical Match Rate
Business MatchName + full addressHighest precision
Phone SearchA verified phone numberHigh precision with validation
Business SearchName + city/stateBroadest 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

ThresholdPurpose
70Minimum name score: reject matches below this
75Confidence threshold for fuzzy search results
80High 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:

  1. 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

  2. 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

  3. 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)

  4. 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

  5. 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


Did this page help you?