Intro
Google has organized his Google Maps Services Platform in three different categories:
- Maps ( StaticMap, DynamicMaps, StreetView, … )
- Routes ( Directions, Roads, Distance, … )
- Places ( Geocoding, Autocomplete, Place Details, … )
Each category includes different services, where the cost of them is related to usage. Every month Google offers $200/month free credit for Maps, Routes and Places, that are equivalent to:
- 100,000 Static maps (Web)
- 28.500 Dynamic maps (Web)
- 25,000 Street View
- 40,000 Directions
- 11.500 Place details
- 10.000 Autocomplete session
Once the monthly free credit has been used, the prices are listed in the Pricing section. Though I’ll suggest this tool, which gives a vision of the monthly cost.
Since the Google Maps Services platform is expensive on large scale, for whoever wants to continue using their services, is forced to count all the API calls, one by one, trying to optimize their project infrastructure, and find what is the most efficient implementation that meet the budget, the user experience and business goals.
Now I want to give you some technical and practical tips, to avoid high monthly billing cost, with little effort, so let’s start!
1. Debouncing Request
Debouncing is a programming practice used to ensure that time-consuming tasks do not fire often, so it doesn’t jeopardise the performance of the web page. In other words, it limits the rate at which a function gets invoked.
The Debounce technique allows “grouping” multiple sequential calls in a single one.
Suppose we are in a situation where we have to generate a static map that varies according to the height and width of a container:
BAD:
const createGoogleMapsUrl = (w,h) => {
const mapURL = `https://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=${w}x${h}&maptype=roadmap&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318&markers=color:red%7Clabel:C%7C40.718217,-73.998284&key=${API_KEY}`
document.querySelector("img").src = mapUrl;
}
window.addEventListener('resize', () => createGoogleMapsUrl(calcW(), calcH()))
If a user resizes the page, the function will be called for each px!
GOOD:
const createGoogleMapsUrl = debounce((w,h) => {
const mapURL = `https://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=${w}x${h}&maptype=roadmap&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318&markers=color:red%7Clabel:C%7C40.718217,-73.998284&key=${API_KEY}`
document.querySelector("img").src = mapUrl;
}, 300)
window.addEventListener('resize', () => createGoogleMapsUrl(calcW(), calcH()))
Wrapping the function createGoogleMapsUrl
inside a debounce will allow us to avoid calling the Google API for every single-pixel since we set a timeout range to avoid repeated requests.
This technique can also be combined with a search input, like autocomplete and/or geocoding services, to avoid requesting each character the user type in an input.
Example: getPlacePredictions
with debounce
autocomplete_input.addEventListener('input', debounce(function() {
let value = this.value;
autocompleteService.getPlacePredictions({
input: value
}, displaySuggestions);
}, 150));
2. Caching
By default, the browser manages the cache, avoiding re-execute requests previously made. Since the browser is responsible for maintaining the cache, we cannot know if a resource was cached, but we could improve our UI knowing this.
The idea is simple, if a page has a static map, we can show it again on another page, without paying more.
Google web service responses include the Cache-Control HTTP header, which indicates the period for which the browser can cache the result of the request.
The Cache-Control
for the static map is Cache-Control: public, max-age=86400
, 86400
ms, so the cache of the browser will be valid for exactly 24 hours.
However, this doesn’t mean that we can use a Proxy to cache all API calls directed to Google it’s forbidden by Terms of Service
Simulate browser caching:
This is not a practical tip, but it allows understanding how browser cache works and eventually trying to simulate it.
const staticMapCache = {};
const fetchStaticMap = async (options) => {
const mapUrl = generateGoogleStaticMapUrl(options);
const hash = getHashCode(mapUrl)
const getInMemoryImage = () => getBase64FromUrl(mapUrl)
if (!staticMapCache[hash]) {
staticMapCache[hash] = await getInMemoryImage()
}
return staticMapCache[hash]
}
Doing so, we’ll have more control over the cache, running a side effect based on the number of calls.
3. Staticmap Size
One of the best ways to reduce costs of Maps Services is to convert dynamic maps to static maps, which are much cheaper, but there are some peculiarities to keep in mind.
The Static Map service if you aren’t Premium offers a max size of 640x640
, so if you request a static map with dimensions 641x641
, Google will offer anyway a 640x640
map, but you will pay for it.
The browser will not use the cache, because it recognizes the two requests as different.
Example: limit StaticMap with Javascript:
If we have a Static Map with dynamic width and height, as in the previous example, we can manipulate the width and height to avoid requests that exceed 640x640px.
const createGoogleMapsUrl = debounce((w,h) => {
const width = w < 640 ? w : 640;
const height = h < 640 ? h : 640;
const mapURL = `https://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=${width}x${height}&maptype=roadmap&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318&markers=color:red%7Clabel:C%7C40.718217,-73.998284&key=${API_KEY}`
document.querySelector("img").src = mapUrl;
}, 300)
window.addEventListener('resize', () => createGoogleMapsUrl(calcW(), calc()))
In this way, we avoid making unnecessary requests (641x641
, 642x642
, etc..) because they’re resized into 640x640, taking advantage of the browser caching.
Tips: By playing with CSS we can set 3 different media queries, so in the worst-case scenario the user will make a maximum of 3 requests to the Static Map service.
Example: limit StaticMap with CSS:
@media (min-width: 1024px) { height: '200px'; }
@media (min-width: 768px) and (max-width: 1024px) { height: '150px'; }
@media (min-width: 320px) and (max-width: 768px) { height: '100px'; }
4. Lazyload request
Lazy loading is a design pattern used to postpone a request until it is not necessary.
A common practice, when you have many images on a web page, is to postpone the images loading only when they are visible on the viewport/screen.
Lazy load Javascript Implementation:
<img data-src="/path/to/img" class="lazy" alt="lazy image" >
document.addEventListener("DOMContentLoaded", function() {
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting) { // is visible on screen
const lazyImage = entry.target
lazyImage.src = lazyImage.dataset.src
lazyImage.classList.remove("lazy");
imgObserver.unobserve(lazyImage);
}
})
});
document.querySelectorAll('img.lazy').forEach((image) => {
imageObserver.observe(image);
})
})
This pattern is generally used for images but it’s replicable for StaticMap, DynamicMap or geocoding.
Pseudo ReactJS Lazy loading Hook:
const LazyStaticMap = (lat, lng) => {
const [ref, isVisible] = useWhenVisibleHook()
return (
<div ref={ref}>
{ isVisible ? (<GoogleStaticMap lat={lat} lng={lng} />) : null }
</div>
)
}
Bonus: A great article about lazy loading Image source
5. Autocomplete
Autocomplete is a service that offers a similar experience to searching for an address on Google Maps, when a user starts typing an address, autocomplete will fill in the rest, also offering reverse geocoding of the address.
There are two different types of billing for Autocomplete:
Per request
Per Session
Per request
Per Request is the default billing option for AutocompleteService
. Charges are applied per keystroke, which could lead to higher billings.
Price:
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.00283$ per each, (2.83$ per 1000) | 0.00227$ per each, (2.27$ per 1000) |
Tips
We can make requests with a debounce
and a minimum keystroke char to avoid numerous requests.
Example:
const onChange = debounce((e) => {
const { value } = e.target
if(value.length > 3){
autocompleteService.getPlacePredictions()
}
}, 250)
input.addEventListener('input', onChange);
Session
Sessions allow to uniquely identify requests to the AutocompleteService
and pay a one-time fee, regardless of the number of characters entered.
Price:
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.017$ per each, (17.00$ per 1000) | 0.0136$ per each, (13.60$ per 1000) |
Autocomplete Widget
handles sessions automatically in the background, grouping the queries typed from the user into a single session used for billing purposes.
Example:
So, if you start typing “New York”, these are the steps that will follow:
Widget Place Autocomplete
will create aSession Token
,- Typing “n”, “ne”, “new”, “new ”, “new y” … “new york” the
Widget
will use the samesession token
created precedently, for every chars. - After the typing is completed and the user has selected one place from the
Widget
, a request is made togetDetails
using the sameSession Token
, so you will not pay for each charter, but only for a single session.
NB: When using AutocompleteService
, Session tokens
must be manually implemented to group autocomplete requests.
AutocompleteService with Session:
// Create a new session token.
let sessionToken = new google.maps.places.AutocompleteSessionToken();
// Pass the token to the autocomplete service.
let autocompleteService = new google.maps.places.AutocompleteService();
autocompleteService.getPlacePredictions({
input: 'Coffee near Lond',
sessionToken: sessionToken
}, displaySuggestions);
Per request vs Session Token:
- If you want to have more control, you should use the
AutocompleteService
instead of theWidgets
. - If you want to have an easy implementation with session token is preferable to use the
Widget
90% of the time it is preferable to use the sessions
because they allow grouping requests
by paying a single fee.
There are other cases where only a few requests are sufficient, in these cases we could take advantage of Per Request method and pay less.
For example, a simple entry of a zip code, can adopt two technique to save money:
- a minimum of 3 digits characters
debounce
pattern
Doing so, 2 requests might be enough to help the user with a satisfying number of suggestions. Since 1 request using per request method is less expensive than a request per session, we could pay less.
6. Filter PlaceDetail request
Google Maps Services platform introduced a new parameter named fields
to Places API, used to filter the request and pay only for the information needed.
This is important because for each information requested, the overall cost will be higher, and by default, the call to PlaceDetail
requires all the information (fields
)
The fields
are divided into 3 types ( SKU ), with three different billing:
Basic
includes: name, address, geometryContact
includes: website, phone number, hoursAtmosphere
includes: review, ratings
1). Basic:
This billing is used when any of these fields are requested:
address_component
, adr_address
, alt_id
, formatted_address
, geometry (location)
, icon
, id
, name
, permanently_closed
, photo
, place_id
, plus_code
, scope
, type
, url
, utc_offset
, vicinity
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.00$ per each (Free) | 0.00$ per each (Free) |
Information of the Basic Field
are included in the cost of the request for Places, and do not involve any additional cost.
2). Contact:
This billing is used when any of these fields are requested:
formatted_phone_number
, international_phone_number
, opening_hours
, website
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.003$ per each, (3.00$ per 1000) | 0.0024$ per each, (2.40$ per 1000) |
3). Atmosphere:
This billing is used when any of these fields are requested:
price_level
, rating
, review
, user_ratings_total
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.005$ per each, (5.00$ per 1000) | 0.004$ per each, (4.00$ per 1000) |
More details here: Google Billing
Tips: Try to understand what information is needed for your application, then add filters
Example: if you just want to know formatted_address
and geometry
, you can save $0.008
by filtering the request.
It may seem derisory, but with a high volume of data in the order of 10,000 requests per day
, we’ll save $2400
per month!
Example PlaceDetails Request:
https://maps.googleapis.com/maps/api/place/details/json?placeid=PLACE_ID&fields=formatted_address,geometry&key=YOUR_API_KEY
Place Service google.maps.places.PlacesService
placesService.getDetails({
placeId: "ChIJlQ-ho9OEQIgRLQeLM-VMRMc",
fields: ["name", "address_component", "formatted_address", "geometry.location"]
}, callback);
Autocomplete Widget google.maps.places.Autocomplete
var autocomplete = new google.maps.places.Autocomplete(
document.getElementById("autocomplete"),
{
fields: ["name", "formatted_address", "geometry.location"]
}
);
7. Optimize the UI
Dynamic maps cost much more than static maps, as we have seen before with 200 dollars we will have 100,000 static maps whereas using dynamic maps with 200 dollars we will have only 28,500 maps.
Switching from dynamic maps to static maps (where possible) we can have a savings of about 70%.
Airbnb example: The dynamic map has only 1 marker
Taking Airbnb as an example, If the search returns only one result, a dynamic map is useless and expensive, in this case, replace the dynamic map with a static map.
Another Example: in other cases we can avoid using an unnecessary static map, removing it or replacing it with a CTA, that has a direct link to google maps.
Summarizing: Requests that require user interaction should only be sent when needed, waiting for a user action like click, scroll, resize.
8. Approximate latitude and longitude
The latitude of a point on Earth is the angle between the equatorial plane and the straight line that passes through that point and the center of the Earth. The longitude of a point on Earth is the angle of a reference meridian to another meridian that passes through that point.
Since one degree of latitude equals approximately 111km (69 miles), we can get this table:
| DECIMAL | DEGREES | DISTANCE |
| ------- | ---------- | -------- |
| 0 | 1 | 111 km |
| 1 | 0.1 | 11.1 km |
| 2 | 0.01 | 1.11 km |
| 3 | 0.001 | 111 m |
| 4 | 0.0001 | 11.1 m |
| 5 | 0.00001 | 1.11 m |
| 6 | 0.000001 | 11.1 cm |
| 7 | 0.0000001 | 1.11 cm |
| 8 | 0.00000001 | 1.11 mm |
1 mile = 1.607 km
An accuracy level of 8 decimal places is not always relevant. To increase cache hit rates, we can approximate the latitude and longitude by rounding to 6 or 2 decimals, taking advantage of browser caching or a proxy caching server.
Example:
const approximate = (lat, lng) => {
return [lat.toFixed(5), lng.toFixed(5)]
}
approximate(42.74625364, 12.48372645) // output: [42.74625, 12.48372]
9. Geocoding API
Using the PlacesService
or the Autocomplete
we can benefit from automatic address search corrections.
Example:
Google correct any user typo, with PlacesService
or Autocomplete
Rone, Italy ==> Rome, Italy // place_id: ChIJN1t_tDeuEmsRUsoyG83frY4
Once the user has specified the address, to retrieve more information and details about the location, an additional request must be made, specifying the place_id
of the searched address.
Example PlaceServices
const placeServices = new google.maps.places.PlacesService(placeInput);
placeServices.getDetails({
placeId: 'ChIJN1t_tDeuEmsRUsoyG83frY4',
fields: [ 'formatted_address',],
}
);
Tips:
- If we have an unambiguous address, and we just want to know its
latitude
andlongitude
we can reduce costs by usingGeocoding
instead ofgetDetails
service. - If we have an ambiguous and incomplete address we need to use a
Place Autocomplete
Example Geocode
request:
https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway,
+Mountain+View,+CA&key=YOUR_API_KEY
10. Directions API waypoints
Routes is a service that calculates directions between locations and creates itineraries, you can use these features with Direction API.
There are two types of Directions API:
- Basic
- Advanced
Basic Directions Pricing:
Less than 10 waypoints
with optimize:false
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.005$ per each, (5.00$ per 1000) | 0.004$ per each, (4.00$ per 1000) |
Advanced Directions Pricing:
More than 10 waypoints
with optimize:true
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.01$ per each, (10.00$ per 1000) | 0.008$ per each, (8.00$ per 1000) |
The optimization is based on Travelling salesman problem
, but in order to save money, we must also minimize the number of requests.
There are cases where requests are redundant, let’s consider this example
We have two routes:
- Route1
A1->B1->C1->D1
- Route2
A2->C1->D1
We can see that there is a common route between the two routes: C1->D1
, so knowing which are the common intersection points we can improve our algorithm to make only the really necessary requests.
Tips:
- Requests using more than 10 waypoints (between 11 and 25) are billed at a higher rate.
- If you do not need up-to-date live traffic information, set the parameter
live traffic
to false
11. Use Embed Maps ( Free )
One of the few free services offered by Google Maps Services Platform, is Embed map
.
Embed map are maps that can be used for free
There are two types of embed map:
- Basic
- Advanced
Embed:
A simple dynamic map, with optional markers and interactions (move, zoom, etc..)
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.00$ per each (Free) | 0.00$ per each (Free) |
Embed Advanced:
It has the same features of basic version (Embed), but with the addition of Directions
, Street View
, or Search mode
.
0 – 100,000 requests | 100,001 – 500,000 requests |
---|---|
0.01$ per each, (10.00$ per 1000) | 0.008$ per each, (8.00$ per 1000) |
I’ll suggest this tool, which lets you create a dynamic embed map easily, and provides an iframe
of the map.
Can Embed maps replace Dynamic maps for free? Not really, it depends on the use and the grade of manipulation you want to have.
First, we need two preconditions:
-
Embed map need a
PlaceId
that can be retrieved manually from Google Map (Free), or by usinggetPlaceDetails
from thePlacesService
(Paid) -
The Terms of Service of Google maps does not allow storing and caching any responses, except for
PlaceId
field which can be saved for future use.
Analyzing the first point, the cost of the call getPlaceDetails
that given an address gives the PlaceId
is 0.017$
per session, much higher than the cost of a normal Dynamic Map
, which is 0.007$
!.
Analyzing instead the second point, we can take advantage of storing the PlaceId
, paying only for the service getPlaceDetails
(once time), store the value of the PlaceId
of an Address
and use the Embed Map
in a freeway.
12. Comment!
Other useful tips are: monitor usage of your API to catch anomalies, set a budget alert, restrict your API keys to specific IP, using a native app to view maps (it’s Free!) instead of a web application…
There isn’t 12° tip, write a comment with your tips about Google Maps Services based on your experience…
Conclusion
The services offered by Google, in large scale can be expensive, there are alternatives like MapBox, Maptiler, Here, etc.. but usually the accuracy and the details are not the same. This makes us reflect on the monopolization of a service that can enslave us, What if Google started charging for Google Analytics or Tag Manager, as it did 2 years ago for Google Maps?
In the next article, I’d like to talk about MapBox, MapTiler and Open Street Map, the advantages, and disadvantages of switching maps service.