90

Using the Google Geocoder v3, if I try to geocode 20 addresses, I get an OVER_QUERY_LIMIT unless I time them to be ~1 second apart, but then it takes 20 seconds before my markers are all placed.

Is there any other way to do it, other than storing the coordinates in advance?

Michiel van Oosterhout
  • 22,839
  • 15
  • 90
  • 132
  • is this still the case? The only restriction i see in the documentation is: "a query limit of 2,500 geolocation requests per day". http://code.google.com/apis/maps/documentation/geocoding/index.html#Limits – russau Sep 19 '10 at 03:58
  • 7
    It's not about the total amount of queries per user per day, it's about the number of queries in a short amount of time, like when querying in a loop. – Michiel van Oosterhout Sep 22 '10 at 19:20
  • We have a business license at our shop and we still run into this issue of unable to handle more than 10 requests per second. The only difference between a business license and a regular developer is that we have a very limit of 100,000 calls per day. – abhi Aug 28 '13 at 20:27
  • @michielvoo Have you solved this? If yes,then kindly help me out. I'm getting OVER_QUERY_LIMIT . [My question](http://stackoverflow.com/questions/29411338/how-to-handle-over-query-limit) in SO. [Fiddle](http://jsfiddle.net/en608jxa/) – Prabs Sep 11 '15 at 06:44

7 Answers7

86

No, there is not really any other way : if you have many locations and want to display them on a map, the best solution is to :

  • fetch the latitude+longitude, using the geocoder, when a location is created
  • store those in your database, alongside the address
  • and use those stored latitude+longitude when you want to display the map.

This is, of course, considering that you have a lot less creation/modification of locations than you have consultations of locations.


Yes, it means you'll have to do a bit more work when saving the locations -- but it also means :

  • You'll be able to search by geographical coordinates
    • i.e. "I want a list of points that are near where I'm now"
  • Displaying the map will be a lot faster
    • Even with more than 20 locations on it
  • Oh, and, also (last but not least) : this will work ;-)
    • You will less likely hit the limit of X geocoder calls in N seconds.
    • And you will less likely hit the limit of Y geocoder calls per day.
Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • I'm curious how you can be sure that the results are correct after some time has passed (let's say, a month). Do you re-query them every once in a while? – Chris Mar 10 '10 at 18:02
  • 2
    If the address *(that you already have in your DB -- else you wouldn't be able to geocode)* doesn't change, chances are pretty low that the latitude/longitude should change. And, of course, each time the address is modified, you should re-query the geocoder, to get the latitude+longitude that correspond to the new address. – Pascal MARTIN Mar 10 '10 at 18:08
  • I have stored the lat/long into the DB and retrieving it from DB via AJAX as an array, but it should then passed again to a java script loop, more over i have received 173 locations from DB. Now it shows me the same OVER_QUERY_LIMIT status. Please advice... – Prabhu M Jul 06 '11 at 15:46
21

You actually do not have to wait a full second for each request. I found that if I wait 200 miliseconds between each request I am able to avoid the OVER_QUERY_LIMIT response and the user experience is passable. With this solution you can load 20 items in 4 seconds.

$(items).each(function(i, item){

  setTimeout(function(){

    geoLocate("my address", function(myLatlng){
      ...
    });

  }, 200 * i);

}
Mohammad Dehghan
  • 17,853
  • 3
  • 55
  • 72
gabeodess
  • 2,006
  • 21
  • 13
  • 6
    but (200 * i) means that pause between each request is increasing. So on 3rd request it's 600, then 800 etc. – Roman Apr 05 '13 at 08:16
  • just remove the '* i' – Chris Sep 02 '13 at 19:50
  • 10
    setTimeout will execute it once. So if I'm correct, (... , 200 * i) will schedule each call separated by 200ms (as oyatek commented), which is what gabeodess wanted to achieve. The current (... , 200) will execute all of them at the same time after 200ms. Or am I missing something? – lepe Jul 10 '14 at 06:37
  • @gabeodess - you should be doing `setInterval` on the number of needed requests, instead of `setTimeout`, and set it to `100` - just in case the address amount will sometime in the future extend the `20` amount. – Rob Scott Feb 23 '15 at 13:59
  • 5
    @gabeodess I've tried your solution but still am getting OVER_QUERY_LIMIT [Fiddle](http://jsfiddle.net/6476m9r0/) – Prabs Sep 11 '15 at 06:46
  • @Prabs I don't see a "setTimeout" or "setInterval" in your Fiddle. Also see the other comments, if you are using setTimeout, you'll want to multiply by "i", but it is probably better to use setInterval as Rob Scott recommended. – gabeodess Sep 12 '15 at 16:00
  • @gabeodess setTimeout is there in first line itself.could you please change any code in that fiddle if possible.as we are planning to pay to google to use more request/min. Is it the only possible solution or is there any other way to do it. – Prabs Sep 14 '15 at 11:58
6

Unfortunately this is a restriction of the Google maps service.

I am currently working on an application using the geocoding feature, and I'm saving each unique address on a per-user basis. I generate the address information (city, street, state, etc) based on the information returned by Google maps, and then save the lat/long information in the database as well. This prevents you from having to re-code things, and gives you nicely formatted addresses.

Another reason you want to do this is because there is a daily limit on the number of addresses that can be geocoded from a particular IP address. You don't want your application to fail for a person for that reason.

Zachary Wright
  • 23,480
  • 10
  • 42
  • 56
2

I'm facing the same problem trying to geocode 140 addresses.

My workaround was adding usleep(100000) for each loop of next geocoding request. If status of the request is OVER_QUERY_LIMIT, the usleep is increased by 50000 and request is repeated, and so on.

And of cause all received data (lat/long) are stored in XML file not to run request every time the page is loading.

gray
  • 145
  • 1
  • 2
  • 1
    Your answer is vague, are you referring to on the server side or is this javascript, if it's the latter, usleep is *not* a function and hence would be incorrect, if it's the former, then I suggest you amend your answer to explicitly state this is server side to avoid ambiguity. – t0mm13b Apr 22 '14 at 16:11
1

EDIT:

Forgot to say that this solution is in pure js, the only thing you need is a browser that supports promises https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/Promise


For those who still needs to accomplish such, I've written my own solution that combines promises with timeouts.

Code:

/*
    class: Geolocalizer
        - Handles location triangulation and calculations.
        -- Returns various prototypes to fetch position from strings or coords or dragons or whatever.
*/

var Geolocalizer = function () {
    this.queue          = [];     // queue handler..
    this.resolved       = [];
    this.geolocalizer = new google.maps.Geocoder();  
};

Geolocalizer.prototype = {
    /*
        @fn: Localize
        @scope: resolve single or multiple queued requests.
        @params: <array> needles
        @returns: <deferred> object
    */
    Localize: function ( needles ) {
        var that = this;
        // Enqueue the needles.
        for ( var i = 0; i < needles.length; i++ ) {
            this.queue.push(needles[i]);
        }
        // return a promise and resolve it after every element have been fetched (either with success or failure), then reset the queue.
        return new Promise (
            function (resolve, reject) {
                that.resolveQueueElements().then(function(resolved){
                  resolve(resolved);
                  that.queue    = [];
                  that.resolved = [];
                });
            }
        );
    },

    /*
        @fn: resolveQueueElements
        @scope: resolve queue elements.
        @returns: <deferred> object (promise)
    */

    resolveQueueElements: function (callback) {
        var that = this;
        return new Promise(
            function(resolve, reject) {
                // Loop the queue and resolve each element.
                // Prevent QUERY_LIMIT by delaying actions by one second.
                (function loopWithDelay(such, queue, i){
                    console.log("Attempting the resolution of " +queue[i-1]);
                    setTimeout(function(){
                        such.find(queue[i-1], function(res){
                           such.resolved.push(res); 
                        });
                        if (--i) {
                            loopWithDelay(such,queue,i);
                        }
                    }, 1000);
                })(that, that.queue, that.queue.length);

                // Check every second if the queue has been cleared.
                var it = setInterval(function(){
                    if (that.queue.length == that.resolved.length) {
                        resolve(that.resolved);
                        clearInterval(it);
                    }
                }, 1000);
            }
        );
    },

    /*
        @fn: find
        @scope: resolve an address from string
        @params: <string> s, <fn> Callback
    */
    find: function (s, callback) {
        this.geolocalizer.geocode({
            "address": s
        }, function(res, status){
           if (status == google.maps.GeocoderStatus.OK) {
               var r = {
                   originalString:  s,
                   lat: res[0].geometry.location.lat(),
                   lng: res[0].geometry.location.lng()
               };
               callback(r);
           }
            else {
                callback(undefined);
                console.log(status);
                console.log("could not locate " + s);
            }
        });
    }
};

Please note that it's just a part of a bigger library I wrote to handle google maps stuff, hence comments may be confusing.

Usage is quite simple, the approach, however, is slightly different: instead of looping and resolving one address at a time, you will need to pass an array of addresses to the class and it will handle the search by itself, returning a promise which, when resolved, returns an array containing all the resolved (and unresolved) address.

Example:

var myAmazingGeo = new Geolocalizer();
var locations = ["Italy","California","Dragons are thugs...","China","Georgia"];
myAmazingGeo.Localize(locations).then(function(res){ 
   console.log(res); 
});

Console output:

Attempting the resolution of Georgia
Attempting the resolution of China
Attempting the resolution of Dragons are thugs...
Attempting the resolution of California
ZERO_RESULTS
could not locate Dragons are thugs...
Attempting the resolution of Italy

Object returned:

enter image description here

The whole magic happens here:

(function loopWithDelay(such, queue, i){
                    console.log("Attempting the resolution of " +queue[i-1]);
                    setTimeout(function(){
                        such.find(queue[i-1], function(res){
                           such.resolved.push(res); 
                        });
                        if (--i) {
                            loopWithDelay(such,queue,i);
                    }
                }, 750);
            })(that, that.queue, that.queue.length);

Basically, it loops every item with a delay of 750 milliseconds between each of them, hence every 750 milliseconds an address is controlled.

I've made some further testings and I've found out that even at 700 milliseconds I was sometimes getting the QUERY_LIMIT error, while with 750 I haven't had any issue at all.

In any case, feel free to edit the 750 above if you feel you are safe by handling a lower delay.

Hope this helps someone in the near future ;)

briosheje
  • 7,356
  • 2
  • 32
  • 54
0

I have just tested Google Geocoder and got the same problem as you have. I noticed I only get the OVER_QUERY_LIMIT status once every 12 requests So I wait for 1 second (that's the minimum delay to wait) It slows down the application but less than waiting 1 second every request

info = getInfos(getLatLng(code)); //In here I call Google API
record(code, info);
generated++; 
if(generated%interval == 0) {
holdOn(delay); // Every x requests, I sleep for 1 second
}

With the basic holdOn method :

private void holdOn(long delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException ex) {
            // ignore
        }
    }

Hope it helps

Hugues
  • 375
  • 1
  • 3
  • 10
0

This worked well for me, after intermittent trial and error over the past couple days. I am using react instant-search-hooks via Algolia with Nextjs and Sanity for a new jobs site for a large company.

Postal Code is a facet for filtering/sorting/query matching that is defined in the algolia index. In another script file, I map out all of these facets (postal code, city, etc); Now that I have 100 returned files they can be mapped out by iterating through a mapped asynchronous import and the lat/lng coords matched to the corresponding zip codes defining a job posting (there are ~2500 postings but only ~100 zip codes to narrow down the coordinates of)

import * as dotenv from "dotenv";
dotenv.config();
import {
  googleNetwork,
  axiosConfig as googleAxiosConfig
} from "../utils/google-axios";
import JSONData from "../../public/data/postalCode/2022/05/26.json";
import fs from "fs";
import { join } from "path";
import type { GeneratedGeolocData } from "../types/algolia";
import { timezoneHelper } from "../utils/timezone-helper";
import { Unenumerate } from "../types/helpers";

let i = 0;
i < JSONData.postalCodes.facetHits.length;
i++;
const getGeoCode = (
  record: Unenumerate<typeof JSONData.postalCodes.facetHits>
) =>
  function () {
    return JSONData.postalCodes.facetHits.map(async (data = record, u) => {
      const googleBase = process.env.NEXT_PUBLIC_GOOGLE_MAPS_BASE_PATH ?? "";
      const googleApiKey =
        process.env.NEXT_PUBLIC_TAKEDA_JOBS_GOOGLE_SERVICES ?? "";
      const params: (string | undefined)[][] = [
        ["address", data.value],
        ["key", googleApiKey]
      ];
      const query = params
        .reduce<string[]>((arr, [k, v]) => {
          if (v) arr.push(`${k}=${encodeURIComponent(v)}`);
          return arr;
        }, [])
        .join("&");
      return await googleNetwork("GET")
        .get(`${googleBase}geocode/json?${query}`, googleAxiosConfig)
        .then(dat => {
          const geoloc = dat.data as GeneratedGeolocData;
          const {
            [0]: Year,
            [2]: Month,
            [4]: Day
          } = new Date(Date.now())
            .toISOString()
            .split(/(T)/)[0]
            .split(/([-])/g);
          const localizedTimestamp = timezoneHelper({
            dateField: new Date(Date.now()),
            timezone: "America/Chicago"
          });
          return setTimeout(
            () =>
              fs.appendFileSync(
                join(
                  process.cwd(),
                  `public/data/geoloc/${Year}/${Month}/${Day}-${[i]}.json`
                ),
                JSON.stringify(
                  {
                    generated: localizedTimestamp,
                    _geoloc: {
                      postalCode: data.value,
                      geolocation: geoloc
                    }
                  },
                  null,
                  2
                )
              ),
            1000
          );
        });
    });
  };
getGeoCode(JSONData.postalCodes.facetHits[i]);

It took a lot less time than anticipated -- under 4 seconds for 100 unique results to generate

Context on the Unenumerate type -- Unenumerate strips the internal repeating unit within an array:

type Unenumerate<T> = T extends Array<infer U> ? U : T;

One of One Hundred Returned Hits in 3.74 seconds

Andrew Ross
  • 1,094
  • 7
  • 16