Nearby-Places Query: Bounding Box + Haversine in MySQL

A reusable pattern for 'what's near me' search on plain MySQL — a fast bounding-box pre-filter followed by an exact Haversine distance calculation, sorted and paginated in SQL.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

For "show me things near this point" without a geospatial database: shrink the candidate set with a cheap BETWEEN bounding box, then compute the real great-circle distance with Haversine directly in SQL so sorting and pagination happen in the database, not after loading every row into Node.

queries/getNearbyPlaces.js
function getNearbyPlaces(pool, { lat, lng, radius, page = 0, limit = 10 }, callback) {
  // Rough degree range for the pre-filter — ~69 miles per degree of latitude.
  const range = radius / 69
  const minLat = lat - range
  const maxLat = lat + range
  const minLng = lng - range
  const maxLng = lng + range
 
  const sql = `
    SELECT pd.*,
      (
        3959 * ACOS(
          COS(RADIANS(?)) * COS(RADIANS(pd.lat)) *
          COS(RADIANS(pd.lng) - RADIANS(?)) +
          SIN(RADIANS(?)) * SIN(RADIANS(pd.lat))
        )
      ) AS distance
    FROM inv_lts_vendor pd
    WHERE (pd.lat BETWEEN ? AND ?) AND (pd.lng BETWEEN ? AND ?)
    HAVING distance <= ?
    ORDER BY distance ASC
    LIMIT ?, ?
  `
 
  pool.query(
    sql,
    [lat, lng, lat, minLat, maxLat, minLng, maxLng, radius, page * limit, limit],
    callback,
  )
}
 
module.exports = { getNearbyPlaces }

Usage #

routes/nearby.js
router.get('/nearby', (req, res) => {
  const { lat, lng, radius, page, limit } = req.query
  getNearbyPlaces(pool, { lat: parseFloat(lat), lng: parseFloat(lng), radius: parseFloat(radius), page: Number(page), limit: Number(limit) }, (err, rows) => {
    if (err) return res.status(400).send({ message: err.message })
    res.status(200).send({ content: rows })
  })
})
Note
  • 3959 is Earth's radius in miles — swap in 6371 for kilometers.
  • The bounding box is a square and the real search area is a circle, so HAVING distance <= ? after the box catches the corners the box over-includes. Drop it if "closest first, no hard cutoff" is all you need.
  • Every value above is a parameterized placeholder (?) — never build this query with template-string interpolation of request input; that's a direct SQL injection path.
© 2026 Sabin Shrestha