# How to Implement Geotagging and Location-Based Search in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 40-55 min
- Compatibility: FlutterFlow Free+ (Google Maps API key required)
- Last updated: March 2026

## TL;DR

Add geotagging in FlutterFlow by auto-capturing device coordinates with geolocator when users create content, storing latitude/longitude and a geohash string in Firestore. For proximity search, use a Cloud Function that queries geohash prefixes to find nearby documents efficiently. Never call Google Geocoding API on every list item render — it will exhaust your API quota within hours.

## Building Location Awareness into a FlutterFlow App

Location-based features — auto-tagging photos, finding nearby businesses, showing 'within 5km' results — require two distinct capabilities: capturing a device's GPS coordinates and querying a database for documents near a given location. Firestore does not have native geospatial query support, so proximity search requires a technique called geohashing. A geohash is a string that encodes latitude and longitude into a compact form where geographically close points share a common prefix. By querying for documents where the geohash starts with a given prefix, you efficiently retrieve all items within a geographic region. This tutorial covers the complete flow from GPS capture to map display.

## Before you start

- FlutterFlow project connected to Firebase/Firestore
- Google Maps API key with Maps SDK for Android, Maps SDK for iOS, and Places API enabled
- geolocator package added to pubspec.yaml (FlutterFlow Pro required)
- Location permission configured in iOS Info.plist and Android AndroidManifest.xml

## Step-by-step guide

### 1. Capture GPS coordinates with geolocator on content creation

Create a Custom Action named getCurrentLocation that returns a Map containing latitude (double) and longitude (double). Add the geolocator: ^10.1.0 package to pubspec.yaml. In the action, call Geolocator.checkPermission() — if denied, call Geolocator.requestPermission(). Then call Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.medium) and return the position coordinates. In FlutterFlow, add this Custom Action to the beginning of your content creation Action Flow (before the Firestore Create Document action). Store the returned coordinates in Page State variables currentLat and currentLng. Use LocationAccuracy.medium rather than high — medium uses cell tower triangulation which is fast and sufficient for location tagging, while high accuracy GPS takes 5-10 seconds and drains battery.

```
// Custom Action: getCurrentLocation
// No parameters
// Returns: String in format "lat,lng" (e.g. "51.5074,-0.1278")
import 'package:geolocator/geolocator.dart';

Future<String> getCurrentLocation() async {
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) return '';
  }
  if (permission == LocationPermission.deniedForever) return '';

  final position = await Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.medium,
  );
  return '${position.latitude},${position.longitude}';
}
```

**Expected result:** When the user opens the content creation page, the action captures their current coordinates within 2-3 seconds and stores them for use in the Firestore write.

### 2. Store coordinates and a geohash in Firestore

Add three fields to your Firestore content collection documents: latitude (Number), longitude (Number), and geohash (String). The geohash is a string like 'gcpvj' that encodes the location. Generate it in a Custom Action using the dart_geohash package or by implementing the algorithm directly. A geohash of length 5 covers approximately a 5km x 5km area — appropriate for most location-based apps. When writing the content document, include all three fields. The latitude and longitude are used for display (showing the exact pin on a map) while the geohash is used for querying (finding documents in a geographic area efficiently without loading every document).

```
// Custom Action: encodeGeohash
// Parameters: latLng (String, format: "lat,lng")
// Returns: String (geohash of length 5)
// Simple geohash encoding — for production, use dart_geohash package
Future<String> encodeGeohash(String latLng) async {
  if (latLng.isEmpty) return '';
  final parts = latLng.split(',');
  if (parts.length != 2) return '';
  final lat = double.tryParse(parts[0]) ?? 0.0;
  final lng = double.tryParse(parts[1]) ?? 0.0;

  // Use dart_geohash package in production:
  // import 'package:dart_geohash/dart_geohash.dart';
  // return GeoHasher().encode(lng, lat, precision: 5);

  // Simplified placeholder — add dart_geohash to pubspec.yaml
  return _encodeSimple(lat, lng, 5);
}

String _encodeSimple(double lat, double lng, int precision) {
  const base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
  var minLat = -90.0, maxLat = 90.0;
  var minLng = -180.0, maxLng = 180.0;
  var isEven = true;
  var bit = 0, ch = 0;
  final result = StringBuffer();

  while (result.length < precision) {
    if (isEven) {
      final mid = (minLng + maxLng) / 2;
      if (lng >= mid) { ch |= (1 << (4 - bit)); minLng = mid; } else { maxLng = mid; }
    } else {
      final mid = (minLat + maxLat) / 2;
      if (lat >= mid) { ch |= (1 << (4 - bit)); minLat = mid; } else { maxLat = mid; }
    }
    isEven = !isEven;
    if (bit < 4) { bit++; } else { result.write(base32[ch]); bit = 0; ch = 0; }
  }
  return result.toString();
}
```

**Expected result:** Content documents in Firestore include latitude, longitude, and a 5-character geohash string alongside the regular content fields.

### 3. Use a Cloud Function for proximity search by geohash prefix

Create an HTTP Cloud Function named searchNearby that accepts lat, lng, and radiusKm as query parameters. In the function, compute the set of geohash prefixes that cover the search radius around the given point. For a 5km radius, a 4-character prefix covers the area. Query Firestore for documents where geohash starts with each of the computed prefixes using a range query (.where('geohash', isGreaterThanOrEqualTo: prefix).where('geohash', isLessThan: prefix + '~')). Merge the results, filter by exact distance using the Haversine formula to remove false positives from the rectangular geohash grid, and return the filtered results as JSON. Call this Cloud Function from FlutterFlow using an API call action.

```
// Cloud Function: searchNearby
// Query params: lat, lng, radiusKm
const functions = require('firebase-functions');
const admin = require('firebase-admin');

exports.searchNearby = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  const { lat, lng, radiusKm = 5 } = req.query;
  if (!lat || !lng) return res.status(400).json({ error: 'lat and lng required' });

  const center = [parseFloat(lat), parseFloat(lng)];
  const radius = parseFloat(radiusKm);

  // Compute geohash prefix length based on radius
  // 5km radius -> prefix length 4, 1km -> length 5
  const prefixLen = radius <= 1 ? 5 : radius <= 10 ? 4 : 3;
  const centerHash = encodeGeohash(center[0], center[1], prefixLen);

  // Query Firestore for documents near the center hash
  const db = admin.firestore();
  const snap = await db.collection('content')
    .where('geohash', '>=', centerHash)
    .where('geohash', '<', centerHash + '~')
    .limit(50)
    .get();

  // Filter by exact distance (Haversine)
  const results = snap.docs
    .map(d => ({ id: d.id, ...d.data() }))
    .filter(doc => {
      const dist = haversineKm(center[0], center[1], doc.latitude, doc.longitude);
      return dist <= radius;
    });

  res.json({ results });
});

function haversineKm(lat1, lng1, lat2, lng2) {
  const R = 6371;
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
```

**Expected result:** The Cloud Function returns a JSON array of documents within the specified radius, sorted by distance, with no false positives from rectangular grid edges.

### 4. Add Google Places Autocomplete for address-based search

For users who want to search by address rather than their current location, add Google Places Autocomplete. In FlutterFlow, go to the Google Maps settings and enable the Places API key. Add a TextField to your search page and an API call action on text change that calls the Google Places Autocomplete API (maps.googleapis.com/maps/api/place/autocomplete/json) with the typed text as the input parameter and your API key. Display the returned predictions in a ListView below the search field. When the user taps a prediction, make a second API call to Places Details to get the exact latitude and longitude of the selected place. Then use these coordinates as the search center for your searchNearby Cloud Function call.

**Expected result:** Typing an address in the search field shows a dropdown of matching places. Selecting a place fetches its coordinates and triggers the proximity search centered on that address.

### 5. Display results on a Google Maps widget with markers

In FlutterFlow, add a Google Maps widget to your search results page. Set the widget to receive a list of map markers generated from the searchNearby results. Each result document becomes a marker with its latitude and longitude. For the marker info window, show the content title and a thumbnail image. Add a Custom Marker Icon action to differentiate content types with different coloured pins. When the map has more than 15-20 markers in view, enable marker clustering by adding the google_maps_cluster_manager package as a Custom Widget — clustering groups nearby markers into numbered circles, preventing visual clutter on dense datasets.

**Expected result:** Search results appear as pins on the Google Maps widget. Tapping a pin shows the content title. Nearby pins are grouped into numbered cluster markers at low zoom levels.

## Complete code example

File: `search_nearby.js`

```javascript
// Cloud Function: searchNearby
// Deploy with: firebase deploy --only functions:searchNearby
// Requires firebase-admin and firebase-functions

const functions = require('firebase-functions');
const admin = require('firebase-admin');

if (!admin.apps.length) admin.initializeApp();

// Geohash encoding (simplified — use geofire-common package in production)
function encodeGeohash(lat, lng, precision) {
  const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
  let minLat = -90, maxLat = 90, minLng = -180, maxLng = 180;
  let isEven = true, bit = 0, ch = 0;
  let result = '';
  while (result.length < precision) {
    if (isEven) {
      const mid = (minLng + maxLng) / 2;
      if (lng >= mid) { ch |= 1 << (4 - bit); minLng = mid; } else { maxLng = mid; }
    } else {
      const mid = (minLat + maxLat) / 2;
      if (lat >= mid) { ch |= 1 << (4 - bit); minLat = mid; } else { maxLat = mid; }
    }
    isEven = !isEven;
    if (bit < 4) { bit++; } else { result += BASE32[ch]; bit = 0; ch = 0; }
  }
  return result;
}

function haversineKm(lat1, lng1, lat2, lng2) {
  const R = 6371;
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a = Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLng / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

exports.searchNearby = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  res.set('Access-Control-Allow-Methods', 'GET');

  const lat = parseFloat(req.query.lat);
  const lng = parseFloat(req.query.lng);
  const radiusKm = parseFloat(req.query.radiusKm || '5');
  const collection = req.query.collection || 'content';

  if (isNaN(lat) || isNaN(lng)) {
    return res.status(400).json({ error: 'lat and lng must be valid numbers' });
  }

  // Choose prefix length based on radius
  const prefixLen = radiusKm <= 1 ? 5 : radiusKm <= 10 ? 4 : 3;
  const centerHash = encodeGeohash(lat, lng, prefixLen);

  const db = admin.firestore();
  const snapshot = await db.collection(collection)
    .where('geohash', '>=', centerHash)
    .where('geohash', '<', centerHash + '~')
    .limit(100)
    .get();

  const results = snapshot.docs
    .map(doc => ({ id: doc.id, ...doc.data() }))
    .filter(doc => {
      if (typeof doc.latitude !== 'number' || typeof doc.longitude !== 'number') return false;
      return haversineKm(lat, lng, doc.latitude, doc.longitude) <= radiusKm;
    })
    .map(doc => ({
      id: doc.id,
      title: doc.title || '',
      latitude: doc.latitude,
      longitude: doc.longitude,
      distanceKm: Math.round(haversineKm(lat, lng, doc.latitude, doc.longitude) * 10) / 10,
    }))
    .sort((a, b) => a.distanceKm - b.distanceKm);

  res.json({ results, count: results.length });
});
```

## Common mistakes

- **Calling Google Geocoding API for reverse geocode on every list item render** — A list of 20 items renders 20 API calls in rapid succession. Google Geocoding API has a free quota of only 40,000 requests per month. A moderately active app with a location-enabled feed will exhaust this quota in days, resulting in empty addresses and billing charges. Fix: Reverse geocode once when the user creates content and store the resulting city/neighbourhood string in Firestore. Bind the pre-stored address string directly to the list item widget instead of calling the API on every render.
- **Storing only latitude and longitude without a geohash in Firestore** — Firestore cannot query by distance natively. To find all documents within 5km of a point using only lat/lng fields, you must load and filter every document — which is slow and expensive as the collection grows. Fix: Always store a geohash string alongside lat/lng when writing location-tagged documents. The geohash enables efficient range queries that reduce the candidate set to only documents in the target geographic area.
- **Not filtering geohash query results by exact Haversine distance** — A geohash prefix query retrieves all documents in a rectangular bounding box, not a circle. Documents in the corners of the rectangle can be significantly outside your intended radius — a 5km radius search can return documents 7km away in the corners. Fix: Always apply a Haversine distance filter after the Firestore geohash query to remove documents that fall in the rectangular grid corners but outside the actual circular search radius.

## Best practices

- Store latitude, longitude, AND geohash on every location-tagged document — all three are needed for different operations
- Choose geohash precision based on your search radius: precision 5 for 1-5km, precision 4 for 5-20km
- Always apply Haversine distance filtering after geohash queries to remove rectangular boundary false positives
- Reverse geocode at write time and store the address string — never call Geocoding API on list render
- Use LocationAccuracy.medium for content tagging and location search — GPS-level accuracy is overkill and slow
- Cache the user's current location in App State and refresh it on page load, not on every action
- Enable marker clustering on Google Maps when displaying more than 15 location results simultaneously

## Frequently asked questions

### Do I need a paid Google Maps API plan for geotagging and proximity search?

Google Maps Platform has a free monthly credit of $200, which covers approximately 28,000 map loads or 40,000 Geocoding API calls. For most apps with under a few thousand daily active users, the free tier is sufficient. GPS coordinate capture via geolocator does not use the Google Maps API and is completely free.

### What geohash precision should I use for my search radius?

As a rule of thumb: precision 6 covers roughly 1km x 0.6km (city block level), precision 5 covers 5km x 5km (neighbourhood level), precision 4 covers 40km x 20km (city level), precision 3 covers 160km x 160km (regional level). For a 5km search radius, precision 4 or 5 both work. Precision 5 will return fewer false positives but may miss documents at the edges of the radius.

### Can I use Firestore's native GeoPoint type instead of storing lat/lng as separate numbers?

Yes. Firestore has a GeoPoint type that stores lat/lng as a single field. However, Firestore still cannot query by distance natively — you still need the geohash field for proximity queries. You can store both: a GeoPoint for display purposes and a geohash string for search. FlutterFlow supports GeoPoint field types in its Firestore panel.

### How do I update a document's geotag if the user edits the content and is in a different location?

In your content edit Action Flow, call getCurrentLocation again to get the new coordinates and encodeGeohash to compute the new geohash. Include latitude, longitude, and geohash in the Firestore Update Document action alongside the other updated fields. The old geohash will be replaced automatically.

### Is there a simpler way to do location search without writing a Cloud Function?

GeoFlutterFire2 is a Flutter package that handles geohash querying client-side, eliminating the need for a Cloud Function. Add it to pubspec.yaml (requires FlutterFlow Pro for code export) and use its query method with a GeoFirePoint center and radius. It handles the geohash prefix computation and Haversine filtering internally.

### How accurate is geohash-based proximity search?

Geohash queries are accurate for finding documents within a geographic area, but the rectangular bounding box means results can include documents slightly outside your radius. The Haversine filter removes these. The core GPS coordinates are accurate to within 3-5 metres using LocationAccuracy.medium, and within 1-2 metres using LocationAccuracy.best.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-geotagging-and-location-based-search-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-geotagging-and-location-based-search-in-flutterflow
