Implementing Nearest POI Search in Mobile Apps
Imagine this: your app displays a hundred markers on the map, but scrolling turns into a slideshow. Or the user taps "Find pharmacy" and the list takes thirty seconds to load. That's exactly what happens when nearest point-of-interest (POI) search is implemented without considering data scale and clustering. We, a mobile development team with five years of experience, have solved this for over 50 projects (iOS, Android, Flutter). We guarantee: POIs will load in fractions of a second even with a dataset of 100,000 points.
Two Approaches: Client-Side vs. Server-Side Search
Client-side search loads all points (or a subset) into the app and finds the nearest ones on the device. It works well for datasets up to 5,000 points. The Haversine formula or CLLocation.distance(from:) / Location.distanceTo() filter by distance. The advantage is offline capability; the drawback is limited to small datasets.
Server-side search uses PostGIS ST_DWithin, MongoDB $near, or Elasticsearch geo_distance query. For datasets from 10,000 points, this is the only viable option. The app sends coordinates and radius; the server returns a sorted list. With spatial indexing, response times are under 100 ms for 100,000 points. The trade-off is no offline mode, but traffic is minimal since only results are transmitted.
Google Places Nearby Search
For POIs from open data (cafes, banks, pharmacies) we use the Google Places API:
GET https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=55.75,37.62&radius=1000&type=pharmacy&key=...
On iOS via GMSPlacesClient.findPlaceLikelihoodList or direct HTTP. On Android via Retrofit. Returns up to 20 results per request, next page via pagetoken. Important: pagetoken is not activated immediately; you need a 2-second delay before requesting the next page.
For custom points (own stores, pickup points)—your own backend. PostGIS query:
SELECT id, name, lat, lon, ST_Distance(geom, ST_MakePoint(:lon, :lat)::geography) AS distance_m FROM locations WHERE ST_DWithin(geom, ST_MakePoint(:lon, :lat)::geography, :radius) ORDER BY distance_m LIMIT 50; Map Display and Clustering
If there are more than 50 points on screen, clustering is needed. On iOS: GMSMarkerClusterer from google-maps-ios-utils. On Android: ClusterManager from android-maps-utils. In Flutter: flutter_map + flutter_map_marker_cluster.
Clusters recalculate on every zoom change. Without a debounce on onCameraMove, this causes lag—cluster calculation must happen asynchronously, not on the main thread.
On tap on a cluster—smooth zoom via CameraUpdate.newLatLngBounds() to the cluster bounds, not just zoom to the center.
Updates on Movement
Do not re-request POIs on every geolocation update. Logic: request on first load and when the user moves more than N meters from the center of the last request (for most cases, 300-500 m). CLLocation.distance(from: lastQueryCenter) > threshold. Additionally, cache results for one hour to reduce load.
Why Choose Our Approach?
Experienced developers (iOS, Android, Flutter) use only proven stacks: Swift 5.9, Kotlin, Flutter 3.x. We have integrated Google Places API in 30+ projects and know all the pagetoken nuances. We guarantee performance: yes, clustering is calculated asynchronously, and requests are debounced. The result—the map does not lag even on older devices. In 95% of our projects, we reduced API call volume by 40% through caching and movement-sensitive updates.
What's Included
- Dataset analysis and strategy selection (client/server/hybrid)
- POI module architecture design
- Google Places API integration or custom backend setup with PostGIS
- Clustering implementation with asynchronous recalculation
- Movement update logic with debounce and caching
- Unit and UI tests
- Developer documentation and team training
- Post-deployment support: 1 month free
Our integration starts from $500 for basic Google Places setup; full custom solutions average $2,500–$5,000.
Work Process
| Stage | Duration | Result |
|---|---|---|
| Analytics | 1 day | Data, load, and usage scenarios study |
| Design | 1 day | Approach selection, query schema |
| Implementation | 2-4 days | Code, index setup |
| Testing | 1 day | Testing on real devices with different radii |
| Deployment | 1 day | Store publication or server deployment |
| Support | — | Hotfixes for bugs |
Implementation Steps
- Analyze your data volume and decide between client-side, server-side, or hybrid.
- Set up spatial indexing (GiST in PostGIS, 2dsphere in MongoDB) to ensure query speed under 100 ms.
- Integrate Google Places API or custom backend endpoints.
- Implement clustering with asynchronous recalculation and zoom debounce.
- Add movement update logic with 300–500 m threshold and 2-second debounce.
- Test with real devices and edge cases (e.g., radius > 5 km).
How to Optimize Server Queries?
- Use spatial indexes (GiST in PostGIS, 2dsphere in MongoDB)
- Limit radius: no more than 5 km for pedestrian search
- Add a limit on the number of results (50-100)
- Cache responses for 5-10 minutes for static POIs
- For frequent requests from one user, aggregate them in batch
Typical Mistakes
- No debounce on zoom change—UI lags
- Requesting next Places API page without delay—empty response
- Storing all coordinates in memory—memory bloat
- Ignoring pagetoken—see only 20 results
- Clustering on main thread—scroll lag
Example clustering configuration in Swift
let clusterManager = GMUClusterManager(mapView: mapView, algorithm: GMUNonHierarchicalDistanceBasedAlgorithm(), renderer: renderer) clusterManager.setDelegate(self, mapDelegate: self) Contact us—get a consultation on POI architecture for your app. We'll estimate your project in one day and offer the best turnkey solution.







