Skip to main content
Coffee Finder retrieves coffee shop data from OpenStreetMap (OSM), a free, community-maintained geographic database. The Overpass API provides real-time querying of OSM data, returning detailed information about nearby cafes and coffee shops.

Data source

Coffee shop data comes from the Overpass API, a read-only API for OpenStreetMap:
  • API endpoint: https://overpass-api.de/api/interpreter
  • Data freshness: Updated continuously by OSM contributors
  • Coverage: Worldwide
  • Cost: Free and open-source
  • Rate limits: Fair-use policy (queries cached for 120 seconds)
OpenStreetMap data is contributed by volunteers worldwide. Coverage quality varies by region, with urban areas typically having more complete data than rural locations.

Coffee shop interface

Each coffee shop is represented by a standardized data structure:
src/app/api/coffee-shops/route.ts
interface CoffeeShop {
  id: string                    // Unique identifier (e.g., "osm-123456")
  name: string                  // Shop name
  address: string              // Formatted street address
  rating?: number              // Not available from OSM
  totalRatings?: number        // Not available from OSM
  isOpen?: boolean             // Not available from OSM
  phone?: string               // Contact phone number
  website?: string             // Shop website URL
  distance?: number            // Distance in meters from user
  location: {                  // Geographic coordinates
    lat: number
    lng: number
  }
  priceLevel?: number          // Not available from OSM
  types?: string[]             // Shop categories
}
Some fields like rating, totalRatings, isOpen, and priceLevel are not available from OpenStreetMap. These are included in the interface for future integration with other data sources (e.g., Google Places API).

API endpoint

Coffee Finder exposes a Next.js API route for fetching coffee shops:
GET /api/coffee-shops?lat={latitude}&lng={longitude}&query={search}&radius={meters}

Query parameters

ParameterTypeRequiredDefaultDescription
latnumberYes-User’s latitude
lngnumberYes-User’s longitude
querystringNo-Search term to filter by name
radiusnumberNo8000Search radius in meters (100-100,000)

Example request

curl "https://coffee-finder.com/api/coffee-shops?lat=37.7749&lng=-122.4194&query=starbucks&radius=5000"

Response format

{
  "shops": [
    {
      "id": "osm-123456",
      "name": "Starbucks Coffee",
      "address": "123 Market Street, San Francisco",
      "phone": "+1 (555) 123-4567",
      "website": "https://www.starbucks.com",
      "distance": 245,
      "location": {
        "lat": 37.7751,
        "lng": -122.4183
      },
      "types": ["cafe"]
    }
  ]
}

Overpass query construction

The API builds dynamic Overpass QL queries based on user parameters:
src/app/api/coffee-shops/route.ts
const normalizedQuery = (query || '').trim()
const escapedQuery = normalizedQuery.replaceAll('"', '').replaceAll('\\', '')

const nameFilter = escapedQuery
  ? `["name"~"${escapedQuery}",i]`
  : ''

const overpassQuery = `
[out:json][timeout:25];
(
  node["amenity"="cafe"]${nameFilter}(around:${radiusMeters},${latitude},${longitude});
  way["amenity"="cafe"]${nameFilter}(around:${radiusMeters},${latitude},${longitude});
  relation["amenity"="cafe"]${nameFilter}(around:${radiusMeters},${latitude},${longitude});
);
out center tags;
`
  • [out:json] - Request JSON format response
  • [timeout:25] - Maximum 25-second query execution
  • node/way/relation - Search all OSM geometry types
  • ["amenity"="cafe"] - Filter for cafe amenities
  • ["name"~"...",i] - Case-insensitive name regex match
  • (around:radius,lat,lng) - Geographic circle search
  • out center tags; - Return center coordinates and all tags

Data normalization

Raw Overpass results are normalized into the CoffeeShop interface:
src/app/api/coffee-shops/route.ts
function normalizeElement(element: OverpassElement, userLat: number, userLng: number): CoffeeShop | null {
  const lat = element.lat ?? element.center?.lat
  const lon = element.lon ?? element.center?.lon

  if (lat === undefined || lon === undefined) {
    return null
  }

  const tags = element.tags || {}
  const name = tags.name || 'Coffee Shop'
  const distance = calculateDistance(userLat, userLng, lat, lon)

  return {
    id: `osm-${element.id}`,
    name,
    address: buildAddress(tags),
    isOpen: undefined,
    phone: tags.phone || tags['contact:phone'],
    website: tags.website || tags['contact:website'],
    distance,
    location: {
      lat,
      lng: lon,
    },
    types: ['cafe'],
  }
}

Address construction

OSM stores addresses as separate tags. The API builds formatted addresses:
src/app/api/coffee-shops/route.ts
function buildAddress(tags: Record<string, string> | undefined): string {
  if (!tags) {
    return 'Address not available'
  }

  const street = tags['addr:street']
  const houseNumber = tags['addr:housenumber']
  const city = tags['addr:city'] || tags['addr:town'] || tags['addr:village']

  const firstLine = [houseNumber, street].filter(Boolean).join(' ').trim()
  if (firstLine && city) {
    return `${firstLine}, ${city}`
  }
  if (firstLine) {
    return firstLine
  }
  if (city) {
    return city
  }
  if (tags['addr:full']) {
    return tags['addr:full']
  }
  return 'Address not available'
}
addr:housenumber = "123"
addr:street = "Main Street"
addr:city = "San Francisco"

→ "123 Main Street, San Francisco"

Distance calculation

The Haversine formula calculates accurate distances on Earth’s surface:
src/app/api/coffee-shops/route.ts
function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
  const radius = 6371000  // Earth's radius in meters
  const dLat = ((lat2 - lat1) * Math.PI) / 180
  const dLng = ((lng2 - lng1) * Math.PI) / 180
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) *
      Math.sin(dLng / 2) * Math.sin(dLng / 2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
  return radius * c
}
This provides:
  • Accurate distances accounting for Earth’s curvature
  • Results in meters
  • Performance suitable for real-time queries
For reference, 1 degree of latitude ≈ 111km. The Haversine formula provides accurate results for distances up to ~10,000km.

Result processing

After normalization, results are sorted and limited:
src/app/api/coffee-shops/route.ts
const shops = elements
  .map((element) => normalizeElement(element, latitude, longitude))
  .filter((shop): shop is CoffeeShop => shop !== null)
  .sort((a, b) => (a.distance || 0) - (b.distance || 0))
  .slice(0, 60)
  1. Map: Convert raw OSM elements to CoffeeShop objects
  2. Filter: Remove invalid entries (null results)
  3. Sort: Order by distance (nearest first)
  4. Limit: Return maximum 60 results
The 60-result limit balances data completeness with performance. Most users interact with only the closest 10-20 coffee shops.

Caching strategy

Next.js automatically caches Overpass responses:
src/app/api/coffee-shops/route.ts
const response = await fetch('https://overpass-api.de/api/interpreter', {
  method: 'POST',
  headers: {
    'Content-Type': 'text/plain;charset=UTF-8',
  },
  body: overpassQuery,
  next: { revalidate: 120 },
})
The revalidate: 120 option:
  • Caches responses for 120 seconds (2 minutes)
  • Reduces load on Overpass API
  • Improves response times for repeated queries
  • Respects fair-use policies

Fallback mock data

If the Overpass API fails, mock data is generated:
src/app/api/coffee-shops/route.ts
try {
  // ... Overpass query ...
  return NextResponse.json({ shops })
} catch (error) {
  console.error('Error fetching coffee shops from Overpass:', error)
  return NextResponse.json({ shops: generateMockCoffeeShops(latitude, longitude) })
}

Mock data generation

src/app/api/coffee-shops/route.ts
function generateMockCoffeeShops(lat: number, lng: number) {
  const coffeeShopNames = [
    'The Cozy Bean',
    'Morning Brew Café',
    'Artisan Coffee House',
    'Bean There, Drank That',
    'The Coffee Corner',
    // ... more names
  ]

  return coffeeShopNames.map((name, index) => {
    const latOffset = (Math.random() - 0.5) * 0.02
    const lngOffset = (Math.random() - 0.5) * 0.02

    return {
      id: `mock-${index}`,
      name,
      address: addresses[index],
      rating: Math.round((3.5 + Math.random() * 1.5) * 10) / 10,
      totalRatings: Math.floor(50 + Math.random() * 500),
      isOpen: Math.random() > 0.3,
      phone: `+1 (555) ${String(Math.floor(100 + Math.random() * 900))}-...`,
      website: `https://example.com/${name.toLowerCase().replaceAll(/\s+/g, '-')}`,
      distance: Math.round(100 + Math.random() * 1500),
      location: {
        lat: lat + latOffset,
        lng: lng + lngOffset,
      },
      priceLevel: Math.ceil(Math.random() * 3),
      types: ['cafe', 'food', 'store'],
    }
  })
}
Mock data ensures the application remains functional during Overpass API outages, but results are fictional and for demonstration purposes only.

Error handling

The API validates all input parameters:
src/app/api/coffee-shops/route.ts
if (!lat || !lng) {
  return NextResponse.json({ error: 'Latitude and longitude are required' }, { status: 400 })
}

const latitude = Number.parseFloat(lat)
const longitude = Number.parseFloat(lng)
const radiusMeters = Number.parseInt(radius, 10)

if (Number.isNaN(latitude) || Number.isNaN(longitude)) {
  return NextResponse.json({ error: 'Invalid latitude or longitude' }, { status: 400 })
}

if (Number.isNaN(radiusMeters) || radiusMeters < 100 || radiusMeters > 100000) {
  return NextResponse.json({ error: 'Radius must be between 100 and 100000 meters' }, { status: 400 })
}

Error responses

HTTP StatusError MessageCause
400Latitude and longitude are requiredMissing lat or lng parameter
400Invalid latitude or longitudeNon-numeric coordinates
400Radius must be between 100 and 100000 metersInvalid radius value

Contact information extraction

OSM supports multiple tag formats for phone and website:
src/app/api/coffee-shops/route.ts
phone: tags.phone || tags['contact:phone'],
website: tags.website || tags['contact:website'],
phone = "+1 555-123-4567"
website = "https://example.com"
contact:phone = "+1 555-123-4567"
contact:website = "https://example.com"
The contact: prefix is an OSM convention for businesses with multiple locations.

Data completeness

Not all coffee shops have complete information. Typical field availability:
FieldAvailabilityNotes
Name~95%Most shops are named in OSM
Address~70%Street addresses vary by region
Phone~30%Often missing or outdated
Website~25%Less commonly added to OSM
Coordinates100%Required for OSM entities
Users can contribute missing data to OpenStreetMap by creating a free account and editing the map. Improvements benefit all OSM-based applications.

Performance metrics

Typical API response times:
  • Cache hit: 50-100ms (Next.js cache)
  • Cache miss (Overpass): 500-2000ms (varies by region and load)
  • Fallback (mock data): 10-20ms
The 120-second cache window ensures most requests hit the fast path.

Build docs developers (and LLMs) love