> ## Documentation Index
> Fetch the complete documentation index at: https://syncupai-preview-playground-prefills.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Placematch

> Match places across multiple geospatial data sources using AI-powered entity resolution

export const PlacematchPlayground = () => {
  const {useState, useEffect, useRef, useCallback} = React;
  const [apiKey, setApiKey] = useState('');
  const [placeName, setPlaceName] = useState('Starbucks');
  const [placeAddress, setPlaceAddress] = useState('');
  const [latitude, setLatitude] = useState(40.7614327);
  const [longitude, setLongitude] = useState(-73.9776216);
  const [isLoading, setIsLoading] = useState(false);
  const [results, setResults] = useState(null);
  const [error, setError] = useState(null);
  const mapContainerRef = useRef(null);
  const mapRef = useRef(null);
  const popupRef = useRef(null);
  const mapboxToken = 'pk.eyJ1IjoibGthczUxIiwiYSI6ImNsejkwNXkydDAyMmIyaXBvMGF6d3g1bDMifQ.A7Ey8Qb14RTMNh27c3s_FQ';
  const examples = [{
    name: 'Starbucks Times Square',
    placeName: 'Starbucks',
    address: '',
    lat: 40.7614327,
    lng: -73.9776216
  }];
  const loadExample = useCallback(example => {
    setPlaceName(example.placeName);
    setPlaceAddress(example.address);
    setLatitude(example.lat);
    setLongitude(example.lng);
    setResults(null);
    setError(null);
  }, []);
  useEffect(() => {
    const updateCodeExamples = () => {
      const codeBlocks = document.querySelectorAll('code');
      codeBlocks.forEach(block => {
        const text = block.textContent;
        if (text && text.includes('Authorization: Bearer')) {
          const displayKey = apiKey || '{YOUR_API_KEY}';
          const updated = text.replace(/Bearer\s+[^\s'"]*/g, `Bearer ${displayKey}`);
          if (updated !== text) {
            block.textContent = updated;
          }
        }
      });
    };
    updateCodeExamples();
  }, [apiKey]);
  const getLineThickness = confidence => {
    if (typeof confidence === 'string') {
      if (confidence === 'VERY_HIGH') return 10;
      if (confidence === 'HIGH') return 7;
      if (confidence === 'MEDIUM') return 4;
      if (confidence === 'LOW') return 2;
    }
    if (confidence >= 0.90) return 10;
    if (confidence >= 0.80) return 7;
    if (confidence >= 0.60) return 4;
    return 2;
  };
  const getLineColor = confidence => {
    if (typeof confidence === 'string') {
      if (confidence === 'VERY_HIGH') return '#3B82F6';
      if (confidence === 'HIGH') return '#3B82F6';
      if (confidence === 'MEDIUM') return '#F59E0B';
      if (confidence === 'LOW') return '#9CA3AF';
    }
    if (confidence >= 0.80) return '#3B82F6';
    if (confidence >= 0.60) return '#F59E0B';
    return '#9CA3AF';
  };
  useEffect(() => {
    if (!document.querySelector('link[href*="mapbox-gl.css"]')) {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = 'https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css';
      document.head.appendChild(link);
    }
    if (!window.mapboxgl) {
      const script = document.createElement('script');
      script.src = 'https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js';
      script.onload = initializeMap;
      document.head.appendChild(script);
    } else {
      initializeMap();
    }
    function initializeMap() {
      const mapboxgl = window.mapboxgl;
      if (!mapContainerRef.current || !mapboxgl || mapRef.current) return;
      mapboxgl.accessToken = mapboxToken;
      const map = new mapboxgl.Map({
        container: mapContainerRef.current,
        style: 'mapbox://styles/mapbox/satellite-v9',
        center: [longitude, latitude],
        zoom: 13
      });
      map.addControl(new mapboxgl.NavigationControl());
      popupRef.current = new mapboxgl.Popup({
        closeButton: false,
        closeOnClick: false
      });
      map.on('load', () => {
        map.addSource('match-lines', {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: []
          }
        });
        map.addLayer({
          id: 'connection-lines',
          type: 'line',
          source: 'match-lines',
          paint: {
            'line-color': ['get', 'color'],
            'line-width': ['get', 'thickness'],
            'line-opacity': 0.7
          }
        });
        map.addSource('input-point', {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: []
          }
        });
        map.addLayer({
          id: 'input-marker',
          type: 'circle',
          source: 'input-point',
          paint: {
            'circle-radius': 10,
            'circle-color': '#EF4444',
            'circle-stroke-width': 3,
            'circle-stroke-color': '#fff'
          }
        });
        map.addSource('match-results', {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: []
          }
        });
        map.addLayer({
          id: 'result-markers',
          type: 'circle',
          source: 'match-results',
          paint: {
            'circle-radius': 8,
            'circle-color': ['get', 'color'],
            'circle-stroke-width': 2,
            'circle-stroke-color': '#fff'
          }
        });
        map.on('mouseenter', 'result-markers', e => {
          map.getCanvas().style.cursor = 'pointer';
          if (e.features && e.features[0] && popupRef.current) {
            const feature = e.features[0];
            const coords = feature.geometry.coordinates;
            const props = feature.properties;
            popupRef.current.setLngLat(coords).setHTML(`
                <div style="padding: 8px; max-width: 300px;">
                  <div style="font-weight: 600; margin-bottom: 4px;">${props.name || 'Unknown'}</div>
                  ${props.full_address ? `<div style="font-size: 12px; color: #666; margin-bottom: 4px;">${props.full_address}</div>` : ''}
                  ${props.source ? `<div style="font-size: 11px; color: #888;">Source: ${props.source}</div>` : ''}
                  ${props.confidence ? `<div style="font-size: 11px; color: #888;">Confidence: ${props.confidence}</div>` : ''}
                  ${props.reasoning ? `<div style="font-size: 11px; color: #666; margin-top: 4px; font-style: italic;">${props.reasoning}</div>` : ''}
                </div>
              `).addTo(map);
          }
        });
        map.on('mouseleave', 'result-markers', () => {
          map.getCanvas().style.cursor = '';
          if (popupRef.current) popupRef.current.remove();
        });
      });
      mapRef.current = map;
    }
    return () => {
      if (mapRef.current) {
        mapRef.current.remove();
        mapRef.current = null;
      }
    };
  }, []);
  useEffect(() => {
    if (mapRef.current && latitude && longitude) {
      mapRef.current.flyTo({
        center: [longitude, latitude],
        zoom: 13
      });
    }
  }, [latitude, longitude]);
  const handleMatch = useCallback(async () => {
    if (!apiKey || !apiKey.trim()) return;
    setIsLoading(true);
    setError(null);
    setResults(null);
    try {
      const requestBody = {
        place: {},
        match_sources: ['reprompt'],
        max_matches: 5
      };
      if (placeName && placeName.trim()) {
        requestBody.place.name = placeName.trim();
      }
      if (placeAddress && placeAddress.trim()) {
        requestBody.place.full_address = placeAddress.trim();
      }
      if (latitude && longitude) {
        requestBody.place.latitude = parseFloat(latitude);
        requestBody.place.longitude = parseFloat(longitude);
      }
      const response = await fetch('https://api.reprompt.io/v2/placematch', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestBody)
      });
      if (!response.ok) {
        if (response.status === 401) {
          throw new Error('Invalid API key');
        }
        const errorData = await response.json();
        throw new Error(errorData.message || `Error: ${response.status}`);
      }
      const data = await response.json();
      setResults(data);
      if (mapRef.current && mapRef.current.isStyleLoaded() && data.results && data.results.length > 0) {
        const inputFeature = {
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [parseFloat(longitude), parseFloat(latitude)]
          },
          properties: {
            name: placeName || 'Input Location'
          }
        };
        const resultFeatures = data.results.filter(r => r.latitude && r.longitude).map(result => ({
          type: 'Feature',
          geometry: {
            type: 'Point',
            coordinates: [result.longitude, result.latitude]
          },
          properties: {
            name: result.name,
            full_address: result.full_address,
            source: result.source,
            confidence: result.confidence,
            reasoning: result.reasoning,
            color: getLineColor(result.confidence)
          }
        }));
        const lineFeatures = data.results.filter(r => r.latitude && r.longitude).map(result => ({
          type: 'Feature',
          geometry: {
            type: 'LineString',
            coordinates: [[parseFloat(longitude), parseFloat(latitude)], [result.longitude, result.latitude]]
          },
          properties: {
            thickness: getLineThickness(result.confidence),
            color: getLineColor(result.confidence)
          }
        }));
        const inputSource = mapRef.current.getSource('input-point');
        if (inputSource) {
          inputSource.setData({
            type: 'FeatureCollection',
            features: [inputFeature]
          });
        }
        const resultSource = mapRef.current.getSource('match-results');
        if (resultSource) {
          resultSource.setData({
            type: 'FeatureCollection',
            features: resultFeatures
          });
        }
        const lineSource = mapRef.current.getSource('match-lines');
        if (lineSource) {
          lineSource.setData({
            type: 'FeatureCollection',
            features: lineFeatures
          });
        }
        const bounds = new window.mapboxgl.LngLatBounds();
        bounds.extend([longitude, latitude]);
        data.results.forEach(result => {
          if (result.latitude && result.longitude) {
            bounds.extend([result.longitude, result.latitude]);
          }
        });
        mapRef.current.fitBounds(bounds, {
          padding: 80,
          maxZoom: 15
        });
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  }, [apiKey, placeName, placeAddress, latitude, longitude]);
  return <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '20px'
  }}>
      {}
      <div style={{
    display: 'flex',
    gap: '8px',
    flexWrap: 'wrap'
  }}>
        {examples.map((example, idx) => <button key={idx} onClick={() => loadExample(example)} style={{
    padding: '8px 16px',
    backgroundColor: '#F3F4F6',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px',
    cursor: 'pointer',
    fontWeight: '500'
  }}>
            {example.name}
          </button>)}
      </div>

      {}
      <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '16px'
  }}>
        {}
        <div>
          <label style={{
    display: 'block',
    marginBottom: '8px',
    fontSize: '14px',
    fontWeight: '500'
  }}>
            API Key (required)
          </label>
          <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="Enter your API key" style={{
    width: '100%',
    padding: '8px',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px'
  }} />
        </div>

        {}
        <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: '12px'
  }}>
          <div>
            <label style={{
    display: 'block',
    marginBottom: '8px',
    fontSize: '14px',
    fontWeight: '500'
  }}>
              Place Name
            </label>
            <input type="text" value={placeName} onChange={e => setPlaceName(e.target.value)} placeholder="e.g., Starbucks" style={{
    width: '100%',
    padding: '8px',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px'
  }} />
          </div>

          <div>
            <label style={{
    display: 'block',
    marginBottom: '8px',
    fontSize: '14px',
    fontWeight: '500'
  }}>
              Address
            </label>
            <input type="text" value={placeAddress} onChange={e => setPlaceAddress(e.target.value)} placeholder="e.g., 1585 Broadway, New York, NY" style={{
    width: '100%',
    padding: '8px',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px'
  }} />
          </div>
        </div>

        {}
        <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: '12px'
  }}>
          <div>
            <label style={{
    display: 'block',
    marginBottom: '8px',
    fontSize: '14px',
    fontWeight: '500'
  }}>
              Latitude
            </label>
            <input type="number" value={latitude} onChange={e => setLatitude(e.target.value)} step="0.000001" style={{
    width: '100%',
    padding: '8px',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px'
  }} />
          </div>

          <div>
            <label style={{
    display: 'block',
    marginBottom: '8px',
    fontSize: '14px',
    fontWeight: '500'
  }}>
              Longitude
            </label>
            <input type="number" value={longitude} onChange={e => setLongitude(e.target.value)} step="0.000001" style={{
    width: '100%',
    padding: '8px',
    border: '1px solid #D1D5DB',
    borderRadius: '6px',
    fontSize: '14px'
  }} />
          </div>
        </div>

        {}
        <button onClick={handleMatch} disabled={isLoading || !apiKey || !apiKey.trim()} style={{
    width: '100%',
    padding: '10px',
    backgroundColor: isLoading || !apiKey || !apiKey.trim() ? '#9CA3AF' : '#3B82F6',
    color: 'white',
    border: 'none',
    borderRadius: '6px',
    fontSize: '14px',
    fontWeight: '500',
    cursor: isLoading || !apiKey || !apiKey.trim() ? 'not-allowed' : 'pointer',
    marginBottom: '16px'
  }}>
          {isLoading ? 'Matching...' : 'Find Matches'}
        </button>

        {}
        {error && <div style={{
    padding: '12px',
    backgroundColor: '#FEE2E2',
    color: '#991B1B',
    borderRadius: '6px',
    fontSize: '14px'
  }}>
            {error}
          </div>}
      </div>

      {}
      <div style={{
    width: '100%',
    height: '500px',
    borderRadius: '8px',
    overflow: 'hidden'
  }} ref={mapContainerRef} />

      {}
      {results && results.results && results.results.length > 0 && <div style={{
    marginTop: '16px'
  }}>
          <Tip>
            Found {results.results.length} {results.results.length === 1 ? 'match' : 'matches'}. Hover over markers to see details. Line thickness indicates confidence score.
          </Tip>
        </div>}

      {}
      {results && results.results && results.results.length > 0 && <div style={{
    marginTop: '20px'
  }}>
          <CodeBlock language="json">
            <code>
              {JSON.stringify({
    results: results.results.slice(0, 3)
  }, null, 2)}
            </code>
          </CodeBlock>
        </div>}
    </div>;
};

## Overview

The [`/placematch`](/api-reference/places/placematch) endpoint helps you match a place against multiple geospatial data sources using AI-powered entity resolution. Given information about a place (name, address, coordinates), we find the right match across sources like Reprompt, Overture, Foursquare, or your own data.

Under the hood, we use a combination of:

* Name similarity
* Geographic proximity and distance
* Category alignment
* Address component matching
* Fine-tuned LLM verification

Each match includes a [`confidence`](/api-reference/places/placematch#response-fields) score, source attribution, and reasoning explanation.

## Interactive Demo

**Try it:** Enter place information below, select data sources, and click 'Find Matches' to see real-time matching results.

<PlacematchPlayground />

## Basic Matching

### Name + Coordinates

Match a place using just its name and coordinates. This is useful when you don't have a full address.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.reprompt.io/v2/placematch \
    --header 'Authorization: Bearer {YOUR_API_KEY}' \
    --header 'Content-Type: application/json' \
    --data '{
      "place": {
        "name": "Starbucks",
        "latitude": 40.7614327,
        "longitude": -73.9776216
      },
      "match_sources": ["reprompt"]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.reprompt.io/v2/placematch',
      headers={
          'Authorization': 'Bearer {YOUR_API_KEY}',
          'Content-Type': 'application/json'
      },
      json={
          'place': {
              'name': 'Starbucks',
              'latitude': 40.7614327,
              'longitude': -73.9776216
          },
          'match_sources': ['reprompt']
      }
  )

  matches = response.json()
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "results": [
    {
      "place_id": "288a25a3-6584-510e-82da-7ebafc328358",
      "name": "Starbucks",
      "full_address": "1290 6th ave, new york, ny 10104",
      "latitude": 40.76075,
      "longitude": -73.97885,
      "category_primary": "coffee_shop",
      "phone": "+1 212-977-4861",
      "website": "https://www.starbucks.com/store-locator/store/88886/",
      "source": "reprompt",
      "distance_m": 128.322674,
      "is_match": true,
      "confidence": "VERY_HIGH",
      "reasoning": "The input POI name is exactly 'Starbucks' and the found POI name is the same. The distance between POIs is 128 meters — slightly above the 100m threshold but still very close and within a reasonable margin for the same store."
    }
  ]
}
```

## GERS Matching with Overture

The [Global Entity Reference System (GERS)](https://docs.overturemaps.org/gers/) provides stable UUID identifiers for places in Overture's 64M+ place dataset. Use placematch to get GERS IDs for your places, enabling data joins and stable references across Overture releases.

### Getting GERS IDs

Match a place against Overture to get its GERS ID and enriched attributes:

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.reprompt.io/v2/placematch \
    --header 'Authorization: Bearer {YOUR_API_KEY}' \
    --header 'Content-Type: application/json' \
    --data '{
      "place": {
        "name": "Blue Bottle Coffee",
        "full_address": "66 Mint St, San Francisco, CA 94103",
        "latitude": 37.7814,
        "longitude": -122.4071
      },
      "match_sources": ["overture"],
      "max_matches": 1
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.reprompt.io/v2/placematch',
      headers={
          'Authorization': 'Bearer {YOUR_API_KEY}',
          'Content-Type': 'application/json'
      },
      json={
          'place': {
              'name': 'Blue Bottle Coffee',
              'full_address': '66 Mint St, San Francisco, CA 94103',
              'latitude': 37.7814,
              'longitude': -122.4071
          },
          'match_sources': ['overture'],
          'max_matches': 1
      }
  )

  # Extract GERS ID from response
  if response.ok and response.json()['results']:
      match = response.json()['results'][0]
      gers_id = match['place_id']  # This is the Overture GERS ID
      print(f"GERS ID: {gers_id}")
      print(f"Confidence: {match['confidence']}")
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "results": [
    {
      "place_id": "d724e74f-017a-4902-9031-bc784ffc1789",
      "name": "Blue Bottle Coffee",
      "full_address": "66 Mint St, San Francisco, CA 94103",
      "latitude": 37.7814,
      "longitude": -122.4071,
      "category_primary": "coffee_shop",
      "source": "overture",
      "confidence": "VERY_HIGH",
      "reasoning": "Exact name and address match with precise coordinates matching Overture Places data"
    }
  ]
}
```

The [`place_id`](/api-reference/places/placematch#response-fields) field contains the GERS ID when matching against Overture.

### Enriching CSV Data with GERS

Match places from a CSV file against Overture to get GERS IDs:

**Input CSV** (`stores.csv`):

```csv theme={null}
company,address,latitude,longitude
Blue Bottle Coffee,"66 Mint St, San Francisco, CA 94103",37.7814,-122.4071
Philz Coffee,"3101 24th St, San Francisco, CA 94110",37.7529,-122.4141
```

**Python Script:**

```python theme={null}
import pandas as pd
import requests

df = pd.read_csv('stores.csv')

# Match each store against Overture to get GERS IDs and enriched data
matches = []
for _, row in df.iterrows():
    response = requests.post(
        'https://api.reprompt.io/v2/placematch',
        headers={'Authorization': 'Bearer {YOUR_API_KEY}'},
        json={
            'place': {
                'name': row['company'],
                'full_address': row['address'],
                'latitude': row['latitude'],
                'longitude': row['longitude']
            },
            'match_sources': ['overture']
        }
    )

    if response.ok and response.json()['results']:
        match = response.json()['results'][0]
        matches.append({
            'company': row['company'],
            'address': row['address'],
            'latitude': row['latitude'],
            'longitude': row['longitude'],
            'category': match.get('category_primary', ''),
            'phone': match.get('phone', ''),
            'website': match.get('website', ''),
            'gers_id': match['place_id']
        })

# Save enriched data back to CSV
enriched_df = pd.DataFrame(matches)
enriched_df.to_csv('stores_with_gers.csv', index=False)
```

**Output CSV** (`stores_with_gers.csv`):

The output includes the original data plus enriched attributes from Overture (category, phone, website) and GERS ID:

```csv theme={null}
company,address,latitude,longitude,category,phone,website,gers_id
Blue Bottle Coffee,"66 Mint St, San Francisco, CA 94103",37.7814,-122.4071,coffee_shop,(510) 653-3394,http://www.bluebottlecoffee.com/cafes/mint-plaza,55d8c5f4-7b23-4713-ab5e-135445190e2d
Philz Coffee,"3101 24th St, San Francisco, CA 94110",37.7529,-122.4141,coffee_shop,(415) 875-9370,http://www.philzcoffee.com/,35be680b-81f3-434d-a423-e3631af6c279
```

## Foursquare OS Places Matching

Match places against [Foursquare OS Places](https://docs.foursquare.com/data-products/docs/places-os-places) to get FSQ IDs:

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.reprompt.io/v2/placematch \
    --header 'Authorization: Bearer {YOUR_API_KEY}' \
    --header 'Content-Type: application/json' \
    --data '{
      "place": {
        "name": "Blue Bottle Coffee",
        "full_address": "66 Mint St, San Francisco, CA 94103",
        "latitude": 37.7814,
        "longitude": -122.4071
      },
      "match_sources": ["foursquare"]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.reprompt.io/v2/placematch',
      headers={
          'Authorization': 'Bearer {YOUR_API_KEY}',
          'Content-Type': 'application/json'
      },
      json={
          'place': {
              'name': 'Blue Bottle Coffee',
              'full_address': '66 Mint St, San Francisco, CA 94103',
              'latitude': 37.7814,
              'longitude': -122.4071
          },
          'match_sources': ['foursquare']
      }
  )

  # Extract FSQ ID from response
  if response.ok and response.json()['results']:
      match = response.json()['results'][0]
      fsq_id = match['place_id']  # This is the Foursquare FSQ ID
      print(f"FSQ ID: {fsq_id}")
  ```

  ```json Response theme={null}
  {
    "results": [
      {
        "place_id": "49ca8f4df964a520b9581fe3",
        "name": "Blue Bottle Coffee",
        "full_address": "66 Mint Plaza",
        "latitude": 37.782584,
        "longitude": -122.407743,
        "category_primary": "Dining and Drinking > Cafe, Coffee, and Tea House > Coffee Shop",
        "phone": "(510) 653-3394",
        "website": "http://www.bluebottlecoffee.com/cafes/mint-plaza",
        "source": "foursquare",
        "distance_m": 143.26,
        "is_match": true,
        "confidence": "VERY_HIGH",
        "reasoning": "The names are identical and the found POI has the same cafe type and official Blue Bottle website..."
      }
    ]
  }
  ```
</CodeGroup>

The [`place_id`](/api-reference/places/placematch#response-fields) field contains the FSQ ID when matching against Foursquare. Once you have FSQ IDs, you can use the [Foursquare Places Feedback API](https://docs.foursquare.com/developer/reference/places-feedback) to flag data errors (closed venues, duplicates, incorrect locations) and help maintain data quality in Foursquare OS Places.

## Combined Matching: Check Multiple Data Sources

Match against both Overture (open data) and Foursquare simultaneously using the [`match_sources`](/api-reference/places/placematch#request-parameters) parameter to see if your place exists in open datasets.

**Use cases:**

* **Data quality checks**: Verify if your places are represented in authoritative open datasets
* **Coverage analysis**: Understand which places are missing from public data sources
* **Multi-source enrichment**: Get both GERS IDs and FSQ IDs in one API call

<CodeGroup>
  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.reprompt.io/v2/placematch',
      headers={'Authorization': 'Bearer {YOUR_API_KEY}'},
      json={
          'place': {
              'name': 'Blue Bottle Coffee',
              'full_address': '66 Mint St, San Francisco, CA 94103',
              'latitude': 37.7814,
              'longitude': -122.4071
          },
          'match_sources': ['overture', 'foursquare']  # Check both sources
      }
  )

  results = response.json()['results']

  for result in results:
      if result['source'] == 'overture':
          print(f"✓ Found in Overture - GERS ID: {result['place_id']}")
      elif result['source'] == 'foursquare':
          print(f"✓ Found in Foursquare - FSQ ID: {result['place_id']}")
  ```

  ```json Response theme={null}
  {
    "results": [
      {
        "place_id": "55d8c5f4-7b23-4713-ab5e-135445190e2d",
        "name": "Blue Bottle Coffee",
        "full_address": "66 Mint Plaza",
        "latitude": 37.782584,
        "longitude": -122.407743,
        "category_primary": "coffee_shop",
        "phone": "(510) 653-3394",
        "website": "http://www.bluebottlecoffee.com/cafes/mint-plaza",
        "source": "overture",
        "confidence": "VERY_HIGH"
      },
      {
        "place_id": "49ca8f4df964a520b9581fe3",
        "name": "Blue Bottle Coffee",
        "full_address": "66 Mint Plaza",
        "latitude": 37.782584,
        "longitude": -122.407743,
        "category_primary": "Dining and Drinking > Cafe, Coffee, and Tea House > Coffee Shop",
        "phone": "(510) 653-3394",
        "website": "http://www.bluebottlecoffee.com/cafes/mint-plaza",
        "source": "foursquare",
        "confidence": "VERY_HIGH"
      }
    ],
    "metadata": {
      "total_results": 2,
      "sources_searched": ["overture", "foursquare"],
      "search_radius_meters": 1000.0
    }
  }
  ```
</CodeGroup>

## Batch Processing: Matching Open Datasets

Match 1000+ places from an open dataset against Overture and Foursquare using async requests with automatic retry.

### Dataset: SF Restaurant Inspections

Download SF restaurant data from [DataSF](https://data.sfgov.org/):

```bash theme={null}
curl -o restaurants.csv 'https://data.sfgov.org/resource/pyih-qa8i.csv?$limit=1000'
```

### Async Script with Retry

Install dependencies: `pip install pandas aiohttp backoff`

```python theme={null}
import asyncio, os, pandas as pd, aiohttp, backoff

API_URL = "https://api.reprompt.io/v2/placematch"
API_KEY = os.getenv("REPROMPT_API_KEY")

@backoff.on_exception(
    backoff.expo, aiohttp.ClientError, max_tries=5,
    giveup=lambda e: isinstance(e, aiohttp.ClientResponseError) and e.status not in [429, 500, 502, 503]
)
async def match_place(session, name, address, lat=None, lon=None):
    place = {"name": name, "full_address": address}
    if lat and lon:
        place.update({"latitude": float(lat), "longitude": float(lon)})

    async with session.post(API_URL, json={
        "place": place,
        "match_sources": ["overture", "foursquare"]
    }) as resp:
        resp.raise_for_status()
        return await resp.json()

async def match_dataset(csv_file):
    df = pd.read_csv(csv_file).head(1000)
    semaphore = asyncio.Semaphore(10)  # 10 concurrent requests
    headers = {"Authorization": f"Bearer {API_KEY}"}

    async def match_row(row):
        async with semaphore:
            try:
                resp = await match_place(
                    session,
                    row['business_name'],
                    f"{row['business_address']}, {row['business_city']}, {row['business_state']}",
                    row.get('business_latitude'),
                    row.get('business_longitude')
                )

                # Organize by source (UUID = Overture, 24-hex = Foursquare)
                by_source = {}
                for m in resp.get('results', []):
                    pid = m.get('place_id', '')
                    if '-' in pid: by_source.setdefault('overture', []).append(m)
                    elif len(pid) == 24: by_source.setdefault('foursquare', []).append(m)

                return {
                    'name': row['business_name'],
                    'overture_id': by_source.get('overture', [{}])[0].get('place_id'),
                    'foursquare_id': by_source.get('foursquare', [{}])[0].get('place_id')
                }
            except Exception as e:
                print(f"Error: {row['business_name']}: {e}")
                return None

    async with aiohttp.ClientSession(headers=headers) as session:
        tasks = [match_row(row) for _, row in df.iterrows()]
        results = [r for r in await asyncio.gather(*tasks) if r]

    pd.DataFrame(results).to_csv('matched.csv', index=False)
    print(f"Matched {len(results)} places")

asyncio.run(match_dataset('restaurants.csv'))
```

**Output:**

```csv theme={null}
name,overture_id,foursquare_id
5A5 Steak Lounge,0fcbf3e8-b44f-4632-8afa-39568a2fc16b,4a011045f964a520d5701fe3
Blue Bottle Coffee,55d8c5f4-7b23-4713-ab5e-135445190e2d,49ca8f4df964a520b9581fe3
```
