Geo queries

Many apps have documents that are indexed by physical locations. For example,your app might allow users to browse stores near their current location.

Solution: Geohashes

Geohash is a system for encoding a(latitude, longitude) pair into a singleBase32 string. In the Geohash system the world is divided into a rectangular grid.Each character of a Geohash string specifies one of 32 subdivisions of theprefix hash. For example the Geohashabcd is one of 32 four-character hashesfully contained within the larger Geohashabc.

The longer the shared prefix between two hashes, the closer they are toeach other. For exampleabcdef is closer toabcdeg thanabcdff. Howeverthe converse is not true! Two areas may be very close to each other whilehaving very different Geohashes:

Geohashes far apart

We can use Geohashes to store and query documents by position inCloud Firestore with reasonable efficiency while only requiring a singleindexed field.

Install helper library

Creating and parsing Geohashes involves some tricky math, so we created helperlibraries to abstract the most difficult parts on Android, Apple, and Web:

Web

//InstallfromNPM.Ifyouprefertouseastatic.jsfilevisit//https://github.com/firebase/geofire-js/releasesanddownload//geofire-common.min.jsfromthelatestversionnpminstall--savegeofire-common

Web

//InstallfromNPM.Ifyouprefertouseastatic.jsfilevisit//https://github.com/firebase/geofire-js/releasesanddownload//geofire-common.min.jsfromthelatestversionnpminstall--savegeofire-common

Swift

Note: This product is not available on watchOS and App Clip targets.
// Add this to your Podfile pod 'GeoFire/Utils'

Kotlin

// Add this to your app/build.gradleimplementation'com.firebase:geofire-android-common:3.2.0'

Java

// Add this to your app/build.gradleimplementation'com.firebase:geofire-android-common:3.1.0'

Store Geohashes

For each document you want to index by location, you will need to store aGeohash field:

Web

import{doc,updateDoc}from'firebase/firestore';// Compute the GeoHash for a lat/lng pointconstlat=51.5074;constlng=0.1278;consthash=geofire.geohashForLocation([lat,lng]);// Add the hash and the lat/lng to the document. We will use the hash// for queries and the lat/lng for distance comparisons.constlondonRef=doc(db,'cities','LON');awaitupdateDoc(londonRef,{geohash:hash,lat:lat,lng:lng});

Web

// Compute the GeoHash for a lat/lng pointconstlat=51.5074;constlng=0.1278;consthash=geofire.geohashForLocation([lat,lng]);// Add the hash and the lat/lng to the document. We will use the hash// for queries and the lat/lng for distance comparisons.constlondonRef=db.collection('cities').doc('LON');londonRef.update({geohash:hash,lat:lat,lng:lng}).then(()=>{// ...});

Swift

Note: This product is not available on watchOS and App Clip targets.
// Compute the GeoHash for a lat/lng pointletlatitude=51.5074letlongitude=0.12780letlocation=CLLocationCoordinate2D(latitude:latitude,longitude:longitude)lethash=GFUtils.geoHash(forLocation:location)// Add the hash and the lat/lng to the document. We will use the hash// for queries and the lat/lng for distance comparisons.letdocumentData:[String:Any]=["geohash":hash,"lat":latitude,"lng":longitude]letlondonRef=db.collection("cities").document("LON")londonRef.updateData(documentData){errorin// ...}

Kotlin

// Compute the GeoHash for a lat/lng pointvallat=51.5074vallng=0.1278valhash=GeoFireUtils.getGeoHashForLocation(GeoLocation(lat,lng))// Add the hash and the lat/lng to the document. We will use the hash// for queries and the lat/lng for distance comparisons.valupdates:MutableMap<String,Any>=mutableMapOf("geohash"tohash,"lat"tolat,"lng"tolng,)vallondonRef=db.collection("cities").document("LON")londonRef.update(updates).addOnCompleteListener{// ...}

Java

// Compute the GeoHash for a lat/lng pointdoublelat=51.5074;doublelng=0.1278;Stringhash=GeoFireUtils.getGeoHashForLocation(newGeoLocation(lat,lng));// Add the hash and the lat/lng to the document. We will use the hash// for queries and the lat/lng for distance comparisons.Map<String,Object>updates=newHashMap<>();updates.put("geohash",hash);updates.put("lat",lat);updates.put("lng",lng);DocumentReferencelondonRef=db.collection("cities").document("LON");londonRef.update(updates).addOnCompleteListener(newOnCompleteListener<Void>(){@OverridepublicvoidonComplete(@NonNullTask<Void>task){// ...}});

Query Geohashes

Geohashes allow us to approximate area queries by joining a set of querieson the Geohash field and then filtering out some false positives:

Web

import{collection,query,orderBy,startAt,endAt,getDocs}from'firebase/firestore';// Find cities within 50km of Londonconstcenter=[51.5074,0.1278];constradiusInM=50*1000;// Each item in 'bounds' represents a startAt/endAt pair. We have to issue// a separate query for each pair. There can be up to 9 pairs of bounds// depending on overlap, but in most cases there are 4.// @ts-ignoreconstbounds=geofire.geohashQueryBounds(center,radiusInM);constpromises=[];for(constbofbounds){constq=query(collection(db,'cities'),orderBy('geohash'),startAt(b[0]),endAt(b[1]));promises.push(getDocs(q));}// Collect all the query results together into a single listconstsnapshots=awaitPromise.all(promises);constmatchingDocs=[];for(constsnapofsnapshots){for(constdocofsnap.docs){constlat=doc.get('lat');constlng=doc.get('lng');// We have to filter out a few false positives due to GeoHash// accuracy, but most will match// @ts-ignoreconstdistanceInKm=geofire.distanceBetween([lat,lng],center);constdistanceInM=distanceInKm*1000;if(distanceInM<=radiusInM){matchingDocs.push(doc);}}}

Web

// Find cities within 50km of Londonconstcenter=[51.5074,0.1278];constradiusInM=50*1000;// Each item in 'bounds' represents a startAt/endAt pair. We have to issue// a separate query for each pair. There can be up to 9 pairs of bounds// depending on overlap, but in most cases there are 4.constbounds=geofire.geohashQueryBounds(center,radiusInM);constpromises=[];for(constbofbounds){constq=db.collection('cities').orderBy('geohash').startAt(b[0]).endAt(b[1]);promises.push(q.get());}// Collect all the query results together into a single listPromise.all(promises).then((snapshots)=>{constmatchingDocs=[];for(constsnapofsnapshots){for(constdocofsnap.docs){constlat=doc.get('lat');constlng=doc.get('lng');// We have to filter out a few false positives due to GeoHash// accuracy, but most will matchconstdistanceInKm=geofire.distanceBetween([lat,lng],center);constdistanceInM=distanceInKm*1000;if(distanceInM<=radiusInM){matchingDocs.push(doc);}}}returnmatchingDocs;}).then((matchingDocs)=>{// Process the matching documents// ...});

Swift

Note: This product is not available on watchOS and App Clip targets.
// Find cities within 50km of Londonletcenter=CLLocationCoordinate2D(latitude:51.5074,longitude:0.1278)letradiusInM:Double=50*1000// Each item in 'bounds' represents a startAt/endAt pair. We have to issue// a separate query for each pair. There can be up to 9 pairs of bounds// depending on overlap, but in most cases there are 4.letqueryBounds=GFUtils.queryBounds(forLocation:center,withRadius:radiusInM)letqueries=queryBounds.map{bound->Queryinreturndb.collection("cities").order(by:"geohash").start(at:[bound.startValue]).end(at:[bound.endValue])}@SendablefuncfetchMatchingDocs(fromquery:Query,center:CLLocationCoordinate2D,radiusInMeters:Double)asyncthrows->[QueryDocumentSnapshot]{letsnapshot=tryawaitquery.getDocuments()// Collect all the query results together into a single listreturnsnapshot.documents.filter{documentinletlat=document.data()["lat"]as?Double??0letlng=document.data()["lng"]as?Double??0letcoordinates=CLLocation(latitude:lat,longitude:lng)letcenterPoint=CLLocation(latitude:center.latitude,longitude:center.longitude)// We have to filter out a few false positives due to GeoHash accuracy, but// most will matchletdistance=GFUtils.distance(from:centerPoint,to:coordinates)returndistance<=radiusInM}}// After all callbacks have executed, matchingDocs contains the result. Note that this code// executes all queries serially, which may not be optimal for performance.do{letmatchingDocs=tryawaitwithThrowingTaskGroup(of:[QueryDocumentSnapshot].self){group->[QueryDocumentSnapshot]inforqueryinqueries{group.addTask{tryawaitfetchMatchingDocs(from:query,center:center,radiusInMeters:radiusInM)}}varmatchingDocs=[QueryDocumentSnapshot]()fortryawaitdocumentsingroup{matchingDocs.append(contentsOf:documents)}returnmatchingDocs}print("Docs matching geoquery:\(matchingDocs)")}catch{print("Unable to fetch snapshot data.\(error)")}

Kotlin

// Find cities within 50km of Londonvalcenter=GeoLocation(51.5074,0.1278)valradiusInM=50.0*1000.0// Each item in 'bounds' represents a startAt/endAt pair. We have to issue// a separate query for each pair. There can be up to 9 pairs of bounds// depending on overlap, but in most cases there are 4.valbounds=GeoFireUtils.getGeoHashQueryBounds(center,radiusInM)valtasks:MutableList<Task<QuerySnapshot>>=ArrayList()for(binbounds){valq=db.collection("cities").orderBy("geohash").startAt(b.startHash).endAt(b.endHash)tasks.add(q.get())}// Collect all the query results together into a single listTasks.whenAllComplete(tasks).addOnCompleteListener{valmatchingDocs:MutableList<DocumentSnapshot>=ArrayList()for(taskintasks){valsnap=task.resultfor(docinsnap!!.documents){vallat=doc.getDouble("lat")!!vallng=doc.getDouble("lng")!!// We have to filter out a few false positives due to GeoHash// accuracy, but most will matchvaldocLocation=GeoLocation(lat,lng)valdistanceInM=GeoFireUtils.getDistanceBetween(docLocation,center)if(distanceInM<=radiusInM){matchingDocs.add(doc)}}}// matchingDocs contains the results// ...}

Java

// Find cities within 50km of LondonfinalGeoLocationcenter=newGeoLocation(51.5074,0.1278);finaldoubleradiusInM=50*1000;// Each item in 'bounds' represents a startAt/endAt pair. We have to issue// a separate query for each pair. There can be up to 9 pairs of bounds// depending on overlap, but in most cases there are 4.List<GeoQueryBounds>bounds=GeoFireUtils.getGeoHashQueryBounds(center,radiusInM);finalList<Task<QuerySnapshot>>tasks=newArrayList<>();for(GeoQueryBoundsb:bounds){Queryq=db.collection("cities").orderBy("geohash").startAt(b.startHash).endAt(b.endHash);tasks.add(q.get());}// Collect all the query results together into a single listTasks.whenAllComplete(tasks).addOnCompleteListener(newOnCompleteListener<List<Task<?>>>(){@OverridepublicvoidonComplete(@NonNullTask<List<Task<?>>>t){List<DocumentSnapshot>matchingDocs=newArrayList<>();for(Task<QuerySnapshot>task:tasks){QuerySnapshotsnap=task.getResult();for(DocumentSnapshotdoc:snap.getDocuments()){doublelat=doc.getDouble("lat");doublelng=doc.getDouble("lng");// We have to filter out a few false positives due to GeoHash// accuracy, but most will matchGeoLocationdocLocation=newGeoLocation(lat,lng);doubledistanceInM=GeoFireUtils.getDistanceBetween(docLocation,center);if(distanceInM<=radiusInM){matchingDocs.add(doc);}}}// matchingDocs contains the results// ...}});

Limitations

Using Geohashes for querying locations gives us new capabilities, but comeswith its own set of limitations:

  • False Positives - querying by Geohash is not exact, and you have tofilter out false-positive results on the client side. These extra readsadd cost and latency to your app.
  • Edge Cases - this query method relies on estimating the distance betweenlines of longitude/latitude. The accuracy of this estimate decreases aspoints get closer to the North or South Pole which means Geohash querieshave more false positives at extreme latitudes.

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2026-02-18 UTC.