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 single Base32 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 the prefix hash. For example the Geohash abcd is one of 32 four-character hashes fully contained within the larger Geohash abc .

The longer the shared prefix between two hashes, the closer they are to each other. For example abcdef is closer to abcdeg than abcdff . However the converse is not true! Two areas may be very close to each other while having very different Geohashes:

Geohashes far apart

We can use Geohashes to store and query documents by position in Cloud Firestore with reasonable efficiency while only requiring a single indexed field.

Install helper library

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

Web

  // 
  
 Install 
  
 from 
  
 NPM 
 . 
  
 If 
  
 you 
  
 prefer 
  
 to 
  
 use 
  
 a 
  
 static 
  
 . 
 js 
  
 file 
  
 visit 
 // 
  
 https 
 : 
 // 
 github 
 . 
 com 
 / 
 firebase 
 / 
 geofire 
 - 
 js 
 / 
 releases 
  
 and 
  
 download 
 // 
  
 geofire 
 - 
 common 
 . 
 min 
 . 
 js 
  
 from 
  
 the 
  
 latest 
  
 version 
 npm 
  
 install 
  
 -- 
 save 
  
 geofire 
 - 
 common 
 

Web

  // 
  
 Install 
  
 from 
  
 NPM 
 . 
  
 If 
  
 you 
  
 prefer 
  
 to 
  
 use 
  
 a 
  
 static 
  
 . 
 js 
  
 file 
  
 visit 
 // 
  
 https 
 : 
 // 
 github 
 . 
 com 
 / 
 firebase 
 / 
 geofire 
 - 
 js 
 / 
 releases 
  
 and 
  
 download 
 // 
  
 geofire 
 - 
 common 
 . 
 min 
 . 
 js 
  
 from 
  
 the 
  
 latest 
  
 version 
 npm 
  
 install 
  
 -- 
 save 
  
 geofire 
 - 
 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.gradle 
 implementation 
  
 ' 
 com 
 . 
 firebase 
 : 
 geofire 
 - 
 android 
 - 
 common 
 : 
 3.2 
 . 
 0 
 ' 
 

Java

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

Store Geohashes

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

Web

 import 
  
 { 
  
 doc 
 , 
  
 updateDoc 
  
 } 
  
 from 
  
 'firebase/firestore' 
 ; 
 // Compute the GeoHash for a lat/lng point 
 const 
  
 lat 
  
 = 
  
 51.5074 
 ; 
 const 
  
 lng 
  
 = 
  
 0.1278 
 ; 
 const 
  
 hash 
  
 = 
  
 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. 
 const 
  
 londonRef 
  
 = 
  
 doc 
 ( 
 db 
 , 
  
 'cities' 
 , 
  
 'LON' 
 ); 
 await 
  
 updateDoc 
 ( 
 londonRef 
 , 
  
 { 
  
 geohash 
 : 
  
 hash 
 , 
  
 lat 
 : 
  
 lat 
 , 
  
 lng 
 : 
  
 lng 
 }); 
  

Web

 // Compute the GeoHash for a lat/lng point 
 const 
  
 lat 
  
 = 
  
 51.5074 
 ; 
 const 
  
 lng 
  
 = 
  
 0.1278 
 ; 
 const 
  
 hash 
  
 = 
  
 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. 
 const 
  
 londonRef 
  
 = 
  
 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 point 
 let 
  
 latitude 
  
 = 
  
 51.5074 
 let 
  
 longitude 
  
 = 
  
 0.12780 
 let 
  
 location 
  
 = 
  
 CLLocationCoordinate2D 
 ( 
 latitude 
 : 
  
 latitude 
 , 
  
 longitude 
 : 
  
 longitude 
 ) 
 let 
  
 hash 
  
 = 
  
 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. 
 let 
  
 documentData 
 : 
  
 [ 
 String 
 : 
  
 Any 
 ] 
  
 = 
  
 [ 
  
 "geohash" 
 : 
  
 hash 
 , 
  
 "lat" 
 : 
  
 latitude 
 , 
  
 "lng" 
 : 
  
 longitude 
 ] 
 let 
  
 londonRef 
  
 = 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ). 
 document 
 ( 
 "LON" 
 ) 
 londonRef 
 . 
 updateData 
 ( 
 documentData 
 ) 
  
 { 
  
 error 
  
 in 
  
 // ... 
 } 
  

Kotlin

 // Compute the GeoHash for a lat/lng point 
 val 
  
 lat 
  
 = 
  
 51.5074 
 val 
  
 lng 
  
 = 
  
 0.1278 
 val 
  
 hash 
  
 = 
  
 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. 
 val 
  
 updates 
 : 
  
 MutableMap<String 
 , 
  
 Any 
>  
 = 
  
 mutableMapOf 
 ( 
  
 "geohash" 
  
 to 
  
 hash 
 , 
  
 "lat" 
  
 to 
  
 lat 
 , 
  
 "lng" 
  
 to 
  
 lng 
 , 
 ) 
 val 
  
 londonRef 
  
 = 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ). 
 document 
 ( 
 "LON" 
 ) 
 londonRef 
 . 
 update 
 ( 
 updates 
 ) 
  
 . 
 addOnCompleteListener 
  
 { 
  
 // ... 
  
 } 
  

Java

 // Compute the GeoHash for a lat/lng point 
 double 
  
 lat 
  
 = 
  
 51.5074 
 ; 
 double 
  
 lng 
  
 = 
  
 0.1278 
 ; 
 String 
  
 hash 
  
 = 
  
 GeoFireUtils 
 . 
 getGeoHashForLocation 
 ( 
 new 
  
 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. 
 Map<String 
 , 
  
 Object 
>  
 updates 
  
 = 
  
 new 
  
 HashMap 
<> (); 
 updates 
 . 
 put 
 ( 
 "geohash" 
 , 
  
 hash 
 ); 
 updates 
 . 
 put 
 ( 
 "lat" 
 , 
  
 lat 
 ); 
 updates 
 . 
 put 
 ( 
 "lng" 
 , 
  
 lng 
 ); 
 DocumentReference 
  
 londonRef 
  
 = 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ). 
 document 
 ( 
 "LON" 
 ); 
 londonRef 
 . 
 update 
 ( 
 updates 
 ) 
  
 . 
 addOnCompleteListener 
 ( 
 new 
  
 OnCompleteListener<Void> 
 () 
  
 { 
  
 @Override 
  
 public 
  
 void 
  
 onComplete 
 ( 
 @NonNull 
  
 Task<Void> 
  
 task 
 ) 
  
 { 
  
 // ... 
  
 } 
  
 }); 
  

Query Geohashes

Geohashes allow us to approximate area queries by joining a set of queries on 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 London 
 const 
  
 center 
  
 = 
  
 [ 
 51.5074 
 , 
  
 0.1278 
 ]; 
 const 
  
 radiusInM 
  
 = 
  
 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. 
 const 
  
 bounds 
  
 = 
  
 geofire 
 . 
 geohashQueryBounds 
 ( 
 center 
 , 
  
 radiusInM 
 ); 
 const 
  
 promises 
  
 = 
  
 []; 
 for 
  
 ( 
 const 
  
 b 
  
 of 
  
 bounds 
 ) 
  
 { 
  
 const 
  
 q 
  
 = 
  
 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 list 
 const 
  
 snapshots 
  
 = 
  
 await 
  
 Promise 
 . 
 all 
 ( 
 promises 
 ); 
 const 
  
 matchingDocs 
  
 = 
  
 []; 
 for 
  
 ( 
 const 
  
 snap 
  
 of 
  
 snapshots 
 ) 
  
 { 
  
 for 
  
 ( 
 const 
  
 doc 
  
 of 
  
 snap 
 . 
 docs 
 ) 
  
 { 
  
 const 
  
 lat 
  
 = 
  
 doc 
 . 
 get 
 ( 
 'lat' 
 ); 
  
 const 
  
 lng 
  
 = 
  
 doc 
 . 
 get 
 ( 
 'lng' 
 ); 
  
 // We have to filter out a few false positives due to GeoHash 
  
 // accuracy, but most will match 
  
 const 
  
 distanceInKm 
  
 = 
  
 geofire 
 . 
 distanceBetween 
 ([ 
 lat 
 , 
  
 lng 
 ], 
  
 center 
 ); 
  
 const 
  
 distanceInM 
  
 = 
  
 distanceInKm 
  
 * 
  
 1000 
 ; 
  
 if 
  
 ( 
 distanceInM 
  
< = 
  
 radiusInM 
 ) 
  
 { 
  
 matchingDocs 
 . 
 push 
 ( 
 doc 
 ); 
  
 } 
  
 } 
 } 
  

Web

 // Find cities within 50km of London 
 const 
  
 center 
  
 = 
  
 [ 
 51.5074 
 , 
  
 0.1278 
 ]; 
 const 
  
 radiusInM 
  
 = 
  
 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. 
 const 
  
 bounds 
  
 = 
  
 geofire 
 . 
 geohashQueryBounds 
 ( 
 center 
 , 
  
 radiusInM 
 ); 
 const 
  
 promises 
  
 = 
  
 []; 
 for 
  
 ( 
 const 
  
 b 
  
 of 
  
 bounds 
 ) 
  
 { 
  
 const 
  
 q 
  
 = 
  
 db 
 . 
 collection 
 ( 
 'cities' 
 ) 
  
 . 
 orderBy 
 ( 
 'geohash' 
 ) 
  
 . 
 startAt 
 ( 
 b 
 [ 
 0 
 ]) 
  
 . 
 endAt 
 ( 
 b 
 [ 
 1 
 ]); 
  
 promises 
 . 
 push 
 ( 
 q 
 . 
 get 
 ()); 
 } 
 // Collect all the query results together into a single list 
 Promise 
 . 
 all 
 ( 
 promises 
 ). 
 then 
 (( 
 snapshots 
 ) 
  
 = 
>  
 { 
  
 const 
  
 matchingDocs 
  
 = 
  
 []; 
  
 for 
  
 ( 
 const 
  
 snap 
  
 of 
  
 snapshots 
 ) 
  
 { 
  
 for 
  
 ( 
 const 
  
 doc 
  
 of 
  
 snap 
 . 
 docs 
 ) 
  
 { 
  
 const 
  
 lat 
  
 = 
  
 doc 
 . 
 get 
 ( 
 'lat' 
 ); 
  
 const 
  
 lng 
  
 = 
  
 doc 
 . 
 get 
 ( 
 'lng' 
 ); 
  
 // We have to filter out a few false positives due to GeoHash 
  
 // accuracy, but most will match 
  
 const 
  
 distanceInKm 
  
 = 
  
 geofire 
 . 
 distanceBetween 
 ([ 
 lat 
 , 
  
 lng 
 ], 
  
 center 
 ); 
  
 const 
  
 distanceInM 
  
 = 
  
 distanceInKm 
  
 * 
  
 1000 
 ; 
  
 if 
  
 ( 
 distanceInM 
  
< = 
  
 radiusInM 
 ) 
  
 { 
  
 matchingDocs 
 . 
 push 
 ( 
 doc 
 ); 
  
 } 
  
 } 
  
 } 
  
 return 
  
 matchingDocs 
 ; 
 }). 
 then 
 (( 
 matchingDocs 
 ) 
  
 = 
>  
 { 
  
 // Process the matching documents 
  
 // ... 
 }); 
  

Swift

Note: This product is not available on watchOS and App Clip targets.
 // Find cities within 50km of London 
 let 
  
 center 
  
 = 
  
 CLLocationCoordinate2D 
 ( 
 latitude 
 : 
  
 51.5074 
 , 
  
 longitude 
 : 
  
 0.1278 
 ) 
 let 
  
 radiusInM 
 : 
  
 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. 
 let 
  
 queryBounds 
  
 = 
  
 GFUtils 
 . 
 queryBounds 
 ( 
 forLocation 
 : 
  
 center 
 , 
  
 withRadius 
 : 
  
 radiusInM 
 ) 
 let 
  
 queries 
  
 = 
  
 queryBounds 
 . 
 map 
  
 { 
  
 bound 
  
 - 
>  
 Query 
  
 in 
  
 return 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ) 
  
 . 
 order 
 ( 
 by 
 : 
  
 "geohash" 
 ) 
  
 . 
 start 
 ( 
 at 
 : 
  
 [ 
 bound 
 . 
 startValue 
 ]) 
  
 . 
 end 
 ( 
 at 
 : 
  
 [ 
 bound 
 . 
 endValue 
 ]) 
 } 
 @ 
 Sendable 
  
 func 
  
 fetchMatchingDocs 
 ( 
 from 
  
 query 
 : 
  
 Query 
 , 
  
 center 
 : 
  
 CLLocationCoordinate2D 
 , 
  
 radiusInMeters 
 : 
  
 Double 
 ) 
  
 async 
  
 throws 
  
 - 
>  
 [ 
 QueryDocumentSnapshot 
 ] 
  
 { 
  
 let 
  
 snapshot 
  
 = 
  
 try 
  
 await 
  
 query 
 . 
 getDocuments 
 () 
  
 // Collect all the query results together into a single list 
  
 return 
  
 snapshot 
 . 
 documents 
 . 
 filter 
  
 { 
  
 document 
  
 in 
  
 let 
  
 lat 
  
 = 
  
 document 
 . 
 data 
 ()[ 
 "lat" 
 ] 
  
 as 
 ? 
  
 Double 
  
 ?? 
  
 0 
  
 let 
  
 lng 
  
 = 
  
 document 
 . 
 data 
 ()[ 
 "lng" 
 ] 
  
 as 
 ? 
  
 Double 
  
 ?? 
  
 0 
  
 let 
  
 coordinates 
  
 = 
  
 CLLocation 
 ( 
 latitude 
 : 
  
 lat 
 , 
  
 longitude 
 : 
  
 lng 
 ) 
  
 let 
  
 centerPoint 
  
 = 
  
 CLLocation 
 ( 
 latitude 
 : 
  
 center 
 . 
 latitude 
 , 
  
 longitude 
 : 
  
 center 
 . 
 longitude 
 ) 
  
 // We have to filter out a few false positives due to GeoHash accuracy, but 
  
 // most will match 
  
 let 
  
 distance 
  
 = 
  
 GFUtils 
 . 
 distance 
 ( 
 from 
 : 
  
 centerPoint 
 , 
  
 to 
 : 
  
 coordinates 
 ) 
  
 return 
  
 distance 
  
< = 
  
 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 
  
 { 
  
 let 
  
 matchingDocs 
  
 = 
  
 try 
  
 await 
  
 withThrowingTaskGroup 
 ( 
 of 
 : 
  
 [ 
 QueryDocumentSnapshot 
 ]. 
 self 
 ) 
  
 { 
  
 group 
  
 - 
>  
 [ 
 QueryDocumentSnapshot 
 ] 
  
 in 
  
 for 
  
 query 
  
 in 
  
 queries 
  
 { 
  
 group 
 . 
 addTask 
  
 { 
  
 try 
  
 await 
  
 fetchMatchingDocs 
 ( 
 from 
 : 
  
 query 
 , 
  
 center 
 : 
  
 center 
 , 
  
 radiusInMeters 
 : 
  
 radiusInM 
 ) 
  
 } 
  
 } 
  
 var 
  
 matchingDocs 
  
 = 
  
 [ 
 QueryDocumentSnapshot 
 ]() 
  
 for 
  
 try 
  
 await 
  
 documents 
  
 in 
  
 group 
  
 { 
  
 matchingDocs 
 . 
 append 
 ( 
 contentsOf 
 : 
  
 documents 
 ) 
  
 } 
  
 return 
  
 matchingDocs 
  
 } 
  
 print 
 ( 
 "Docs matching geoquery: 
 \( 
 matchingDocs 
 ) 
 " 
 ) 
 } 
  
 catch 
  
 { 
  
 print 
 ( 
 "Unable to fetch snapshot data. 
 \( 
 error 
 ) 
 " 
 ) 
 } 
  

Kotlin

 // Find cities within 50km of London 
 val 
  
 center 
  
 = 
  
 GeoLocation 
 ( 
 51.5074 
 , 
  
 0.1278 
 ) 
 val 
  
 radiusInM 
  
 = 
  
 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. 
 val 
  
 bounds 
  
 = 
  
 GeoFireUtils 
 . 
 getGeoHashQueryBounds 
 ( 
 center 
 , 
  
 radiusInM 
 ) 
 val 
  
 tasks 
 : 
  
 MutableList<Task<QuerySnapshot> 
>  
 = 
  
 ArrayList 
 () 
 for 
  
 ( 
 b 
  
 in 
  
 bounds 
 ) 
  
 { 
  
 val 
  
 q 
  
 = 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ) 
  
 . 
 orderBy 
 ( 
 "geohash" 
 ) 
  
 . 
 startAt 
 ( 
 b 
 . 
 startHash 
 ) 
  
 . 
 endAt 
 ( 
 b 
 . 
 endHash 
 ) 
  
 tasks 
 . 
 add 
 ( 
 q 
 . 
 get 
 ()) 
 } 
 // Collect all the query results together into a single list 
 Tasks 
 . 
 whenAllComplete 
 ( 
 tasks 
 ) 
  
 . 
 addOnCompleteListener 
  
 { 
  
 val 
  
 matchingDocs 
 : 
  
 MutableList<DocumentSnapshot> 
  
 = 
  
 ArrayList 
 () 
  
 for 
  
 ( 
 task 
  
 in 
  
 tasks 
 ) 
  
 { 
  
 val 
  
 snap 
  
 = 
  
 task 
 . 
 result 
  
 for 
  
 ( 
 doc 
  
 in 
  
 snap 
 !! 
 . 
 documents 
 ) 
  
 { 
  
 val 
  
 lat 
  
 = 
  
 doc 
 . 
 getDouble 
 ( 
 "lat" 
 ) 
 !! 
  
 val 
  
 lng 
  
 = 
  
 doc 
 . 
 getDouble 
 ( 
 "lng" 
 ) 
 !! 
  
 // We have to filter out a few false positives due to GeoHash 
  
 // accuracy, but most will match 
  
 val 
  
 docLocation 
  
 = 
  
 GeoLocation 
 ( 
 lat 
 , 
  
 lng 
 ) 
  
 val 
  
 distanceInM 
  
 = 
  
 GeoFireUtils 
 . 
 getDistanceBetween 
 ( 
 docLocation 
 , 
  
 center 
 ) 
  
 if 
  
 ( 
 distanceInM 
  
< = 
  
 radiusInM 
 ) 
  
 { 
  
 matchingDocs 
 . 
 add 
 ( 
 doc 
 ) 
  
 } 
  
 } 
  
 } 
  
 // matchingDocs contains the results 
  
 // ... 
  
 } 
  

Java

 // Find cities within 50km of London 
 final 
  
 GeoLocation 
  
 center 
  
 = 
  
 new 
  
 GeoLocation 
 ( 
 51.5074 
 , 
  
 0.1278 
 ); 
 final 
  
 double 
  
 radiusInM 
  
 = 
  
 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 
 ); 
 final 
  
 List<Task<QuerySnapshot> 
>  
 tasks 
  
 = 
  
 new 
  
 ArrayList 
<> (); 
 for 
  
 ( 
 GeoQueryBounds 
  
 b 
  
 : 
  
 bounds 
 ) 
  
 { 
  
 Query 
  
 q 
  
 = 
  
 db 
 . 
 collection 
 ( 
 "cities" 
 ) 
  
 . 
 orderBy 
 ( 
 "geohash" 
 ) 
  
 . 
 startAt 
 ( 
 b 
 . 
 startHash 
 ) 
  
 . 
 endAt 
 ( 
 b 
 . 
 endHash 
 ); 
  
 tasks 
 . 
 add 
 ( 
 q 
 . 
 get 
 ()); 
 } 
 // Collect all the query results together into a single list 
 Tasks 
 . 
 whenAllComplete 
 ( 
 tasks 
 ) 
  
 . 
 addOnCompleteListener 
 ( 
 new 
  
 OnCompleteListener<List<Task 
< ? 
>>> () 
  
 { 
  
 @Override 
  
 public 
  
 void 
  
 onComplete 
 ( 
 @NonNull 
  
 Task<List<Task 
< ? 
>>>  
 t 
 ) 
  
 { 
  
 List<DocumentSnapshot> 
  
 matchingDocs 
  
 = 
  
 new 
  
 ArrayList 
<> (); 
  
 for 
  
 ( 
 Task<QuerySnapshot> 
  
 task 
  
 : 
  
 tasks 
 ) 
  
 { 
  
 QuerySnapshot 
  
 snap 
  
 = 
  
 task 
 . 
 getResult 
 (); 
  
 for 
  
 ( 
 DocumentSnapshot 
  
 doc 
  
 : 
  
 snap 
 . 
 getDocuments 
 ()) 
  
 { 
  
 double 
  
 lat 
  
 = 
  
 doc 
 . 
 getDouble 
 ( 
 "lat" 
 ); 
  
 double 
  
 lng 
  
 = 
  
 doc 
 . 
 getDouble 
 ( 
 "lng" 
 ); 
  
 // We have to filter out a few false positives due to GeoHash 
  
 // accuracy, but most will match 
  
 GeoLocation 
  
 docLocation 
  
 = 
  
 new 
  
 GeoLocation 
 ( 
 lat 
 , 
  
 lng 
 ); 
  
 double 
  
 distanceInM 
  
 = 
  
 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 comes with its own set of limitations:

  • False Positives- querying by Geohash is not exact, and you have to filter out false-positive results on the client side. These extra reads add cost and latency to your app.
  • Edge Cases- this query method relies on estimating the distance between lines of longitude/latitude. The accuracy of this estimate decreases as points get closer to the North or South Pole which means Geohash queries have more false positives at extreme latitudes.
Design a Mobile Site
View Site in Mobile | Classic
Share by: