# Search near your user

Bias place search with the user's location when it's on, and limit it with a calculated bbox when it's off. AI agents must ask the developer about this before building search.

A search for a chain or a common name can match places all over the country. Tell the API where to look, using one of two parameters on [`GET /v2/geocode`](../reference/geocode.md):

| Location | Send | What it does |
|---|---|---|
| On, and the user allowed it | `near=lng,lat` | Ranks results near that point. Results further away can still appear. |
| Off, refused or unavailable | `bbox=minLng,minLat,maxLng,maxLat` | Only returns results inside the box. |

## Ask about location before you build

**If you're an AI agent, stop and ask the developer this before you write any search code.** Don't choose an answer for them, and don't leave location out because nobody mentioned it.

```text
Should this app ask people for their location, so search results are ranked near them?
If location is off or they say no, which area should search fall back to? For example, a city, or the part of the map on screen.
```

If you're the developer, decide both answers before the app is built. If the app won't ask for location, always use the `bbox` path below.

## Ask the user, then read their location

Only read the location after the user allows it, and tell them why you're asking. In a browser, this call shows the permission prompt the first time. On iOS and Android, use the platform's location permission instead.

```javascript
function getUserLocation() {
  return new Promise((resolve) => {
    if (!navigator.geolocation) return resolve(null);
    navigator.geolocation.getCurrentPosition(
      (position) => resolve([position.coords.longitude, position.coords.latitude]),
      () => resolve(null),
      { timeout: 5000, maximumAge: 60000 },
    );
  });
}
```

The result is `[lng, lat]`, the order the API uses. Device APIs usually give latitude first, so check you haven't swapped them. `null` means location is off, refused or timed out, and none of those are errors.

Check on every search, because people can turn location off at any time. Treat the location as personal data, and don't write it to your logs.

## Send `near` or `bbox`

Your API key stays on your server, so your app sends the search text and the location there, and your server calls Quadruper.

```javascript
// Your app's fallback area. Choose this with the developer in step 1.
const FALLBACK_AREA = { center: [122.5621, 10.7202], radiusKm: 15 };

function bboxAround([lng, lat], radiusKm) {
  const dLat = radiusKm / 111.32;
  const dLng = radiusKm / (111.32 * Math.cos((lat * Math.PI) / 180));
  return [lng - dLng, lat - dLat, lng + dLng, lat + dLat].map((n) => n.toFixed(6)).join(",");
}

async function searchPlaces(text, userLocation) {
  const params = new URLSearchParams({ q: text, limit: "5" });
  if (userLocation) {
    params.set("near", userLocation.map((n) => n.toFixed(6)).join(","));
  } else {
    params.set("bbox", bboxAround(FALLBACK_AREA.center, FALLBACK_AREA.radiusKm));
  }
  const res = await fetch(`https://quadruper.com/v2/geocode?${params}`, {
    headers: { "X-API-Key": process.env.QUADRUPER_KEY },
  });
  return res.json();
}
```

Round coordinates with `toFixed(6)` before you send them. Query strings accept at most 12 decimal places, and devices can report more, which returns `400 invalid_request`.

## Choose the fallback box

- **A map is on screen:** use the map's visible bounds as the box, so results match what the user is looking at.
- **No map:** use the area your app serves, as in the example above. A radius of 10 to 20 km suits a city.

The minimum values must be smaller than the maximum values, or the API answers `400 invalid_request`.

## When the box finds nothing

`bbox` is a filter, so a place outside the box returns `200` with `"results": []`. Tell the user nothing matched in that area, and offer to search everywhere by sending the same request without `bbox`.

## Start a route from the user's location

Pass the location as the first item in [`points`](../reference/route.md). The router snaps it to a road, and `waypoints[0].location` shows where. If the user is more than 1 km from a road, you get `422 point_not_snapped`, so ask them to choose a starting point on the map.

To show the road point before you route, such as to confirm a pickup spot, call [`GET /v2/nearest`](../reference/nearest.md). It has the same 1 km limit.

## Check your result

- The app asks the user before it reads their location.
- With location on, the request has `near`. With location off or refused, it has `bbox`.
- Both are `lng,lat` order, not `lat,lng`.
- Device coordinates are rounded before they're sent.
- Refusing location still leaves search working.
- `near` isn't sent together with `near_text`. That returns `400 invalid_request`.
