Ever wondered how
Google
works its magic with Maps? 🗺️ They've got these cool Maps URLs that
hold location secrets, like where to find panoramas and other goodies.
🌍🌆
I got curious during my little pet project and needed some of that
location jazz and panorama ID pizzazz. But guess what? Google keeps the
recipe for those URLs hush-hush. 🤫 So, I put on my virtual Sherlock
Holmes hat 🕵️ and went on an adventure to reverse-engineer the
map-magic. 🚀 Now, I'm here to share the treasure map with you! X marks
the spot! 📍🗝️
Use cases
User shares a location or panorama from the Google Maps app or
web-site to our app. As a result, a shortened link is received. Due to
Google's redirection from the shortened URL to the full URL, we should
send an HTTP request to the shortened link to receive the full URL. This
URL will be parsed to gain useful information.
There are several cases of shared locations:
- Point
- Place
- Panorama
- User panorama
- Search for a location, place or address
- Special case #1: Place + Map moved
- Special case #2: Place + Panorama
Other sections:
- Web-API: Panorama preview
- Web-API: Panorama metadata
- Web-API: Place information
- Web-API: Sign your request
- Android Place API
- Android Geocoder
- CID issue
- Conclusion
1. Point
Web-browser shared link:
goo.gl/maps/Up94VLPN5kPJ74WEA
Unshortened link:
www.google.com/maps/search/48.718116,+2.627758?entry=tts&shorturl=1
I use domain ".com" in all my examples, but globally it can be any of supported ones: supported_domains.
Query segment may be discarded:
www.google.com/maps/search/48.718116,+2.627758
Plus sign is an URL encoded space sign.
So, we can get the location of the place: (lat: 48.718116, lon:
2.627758). Nevertheless, I assume that there are cases when value of
this attribute contains not coordinates, but rather an address or a
description of the place.
The link from the mobile app has a different domain:
maps.app.goo.gl/7qWFiAfvRsBEKTz56
is redirected to:
www.google.com/maps/place/
-13.165331,-72.548991/
data =
!4m6
!3m5
!1s0
!7e2
!8m2
!3d -13.1653311
!4d -72.5489906
Сoordinates are also provided as path attributes, and additionally, they are present in the data path attribute.
The "data" segment consists of sections, each
starting with an exclamation mark "!", followed by a digit, a letter,
and the data. Let's describe some of them:
- !4m6 - "m" stands for a container, first
"4" stands for "container of containers", first "3" - "container of
values", last "6" is the number of included items
- !8m2 - coordinates container, two children included
- !3d - latitude, double value
- !4d - longitude, double value
A point can be represented as a place with coordinates in DMS format, serving as a name:
www.google.com/maps/place/
48°43'05.4"N+2°37'40.4"E/
@48.7181667,2.6278889,17z/
data =
!3m1
!4b1
!4m4
!3m3
!8m2
!3d 48.7181667
!4d 2.6278889
- 17z - stands for "zoom level" = 17
2. Place
Web-link:
www.google.com/maps/place/
Buckingham+Palace/
@51.5016211,-0.1428878,17z/
data =
!3m1
!5s 0x487605276d38fb6b:0xe1c60228d7946675
!4m6
!3m5
!1s 0x48760520cd5b5eb5:0xa26abf514d902a7
!8m2
!3d 51.5016211
!4d -0.1428878
!16z L20vMDE5OGc
!5m1
!1e4
App-link:
www.google.com/maps/place/
Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/
data =
!4m2
!3m1
!1s 0x48760520cd5b5eb5:0xa26abf514d902a7
The useful information is limited just to the place name.
3. Panorama
Web-link:
www.google.com/maps/
@48.8583445,2.2943296,2a,75y,313.07h,81.5t/
data =
!3m7
!1e1
!3m5
!1s _BgQhZS8mY-Zix5BdCt2mA
!2e0
!6s https://streetviewpixels-pa.googleapis.com/v1/thumbnail
? panoid = _BgQhZS8mY-Zix5BdCt2mA
& cb_client = maps_sv.tactile.gps
& w = 203
& h = 100
& yaw = 258.24527
& pitch = 0
& thumbfov = 100
!7i 13312
!8i 6656
!5m1
!1e4
- 1e1 - is an indicator that data contains panorama info.
The panorama ID is always located in the "!1s" field and followed either with "!2e0" or "!2e10", where "0" indicates that the panorama belongs to Google and "10"
- to the private person (user). Moreover, for user panoramas, this
string won't exactly be the panorama ID, but we'll get to that later.
So, the panorama ID can be preceded by such a set of characters:
- !3m7!1e1!3m5!1s
- !3m6!1e1!3m4!1s
- !3m4!1e1!3m2!1s
@-path attribute contains camera settings:
- 48.8583445,2.2943296 - coordinates (latitude, longitude)
- 75y - vertical view angle, limited: [15, 90]
- 313.07h - heading / bearing / azimuth
- 81.5t - tilt / pitch
IMPORTANT: all slashes in data section must be encoded as "%2F".
App-link:
www.google.com/maps/
@48.858345,2.2943299,0a,75y,312.57h,84.6t/
data =
!3m4
!1e1
!3m2
!1s _BgQhZS8mY-Zix5BdCt2mA
!2e0
4. User panorama
Web-link:
www.google.com/maps/
@50.45007,30.5238129,3a,48.9y,228.12h,97.74t/
data =
!3m6
!1e1
!3m4
!1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5
!2e10
!7i 12082
!8i 6041
So, here we can see the "!2e10" ending
after the panorama ID section, and it indicates that this is a user
panorama. The issue is that the panorama ID in such URLs is not the ID
of the actual panorama; it's more like a "creator panorama ID" which
can't be used to obtain this panorama by API. I haven't been able to
figure out how to translate it into the actual panorama ID.
The only option we have is to use the shooting location's
coordinates and hope that Google consistently finds the desired
panorama. My expectation is based on the fact that user-generated
panoramas are individual and occupy specific spots with some accuracy on
the map. Furthermore, these custom panoramas lack the "history stack"
feature ("history stack" allows you to select a panorama based on its capture date for a particular location). Nevertheless, if I come across a relevant case, then I will find an alternative way to obtain the correct panorama.
App-link almost the same:
www.google.com/maps/
@50.4500732,30.5238113,0a,75y,231.11h,93.48t/
data =
!3m4
!1e1
!3m2
!1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5
!2e10
But there is one nuance: not all user panoramas
that open in the browser can be accessed through the API. This situation
arises because user panoramas are associated with the Google Photos
service, and in order for a panorama to be opened through the Street
View Panorama API, it must have an identifier in the Google Street View
service. Unfortunately, this is not always the case. To check if a
panorama is registered in the Google Street View service, you can look
for a blue dot at the location of the panorama. If it's not there, then it will be impossible to find this panorama through the Google Street View API.
For example, there is a registered panorama, blue dot is under the Pegman:
|
Chernihiv. Hotel "Ukraine" after being hit by a ruZZian missile. |
- photo ID: AF1QipN9GteezKnVzb23gMzWsirXHLDqeTreSvuQzIsc
- panorama ID: CAoSLEFGMVFpcE45R3RlZXpLblZ6YjIzZ016V3NpclhITERxZVRyZVN2dVF6SXNj
- location: 51.494505427553,31.294799751616
There is a not registered panorama, no blue dot under the Pegman:
- photo ID: AF1QipOi38JA6bFk_E9ERR7kNDPkMt6VMIxvjQHkpGSv
- location: 51.5250583,31.3265528
5. Search for a location, place or address
These links are generated in the mobile app only, but not for all search results; usually, they are place-links.
www.google.com/maps?q=50.813588,-2.475488
www.google.com/maps?q=London,+Great+Britain
www.google.com/maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA
6. Special case #1: Place + Map moved
- Click on any place-marker.
- Move map camera away.
- Click on Share button on the Marker info window.
Example:
- Place: Empire State Building
- Map camera: East River
www.google.com/maps/place/
Empire+State+Building/
@40.7321378,-73.9691401,17.87z/
data =
!3m1
!5s 0x8b398fecd1aea119:0x76fa1e3ac5a94c70
!4m6
!3m5
!1s 0x89c259a9b3117469:0xd134e199a405a163
!8m2
!3d 40.7484405
!4d -73.9856644
!16z L20vMDJuZF8
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
40.7321378,-73.9691401 = 40°43'55.7"N, 73°58'08.9"W - East River
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building
Result:
- place = marker place name
- @location = map camera location
- data-location = place marker location
7. Special case #2: Place + Panorama
- Select any place marker.
- Turn on Street View layer.
- Move map camera away.
- Open panorama on any place, not the original place.
- Click Share on panorama view (under triple dots menu on top right corner).
Example:
- Place: Empire State Building
- Panorama: Flatiron Building
www.google.com/maps/place/
Empire+State+Building/
@40.7413886,-73.9891371,3a,75y,230.15h,116.97t/
data =
!3m6
!1e1
!3m4
!1s nhbQjZVsxbtfxb9rTUhZmQ
!2e0
!7i 16384
!8i 8192
!4m6
!3m5
!1s 0x89c259a9b3117469:0xd134e199a405a163
!8m2
!3d 40.7484405
!4d -73.9856644
!16z L20vMDJuZF8
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
40.7413886,-73.9891371 = 40°44'29.0"N, 73°59'20.9"W - Flatiron Building
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building
Result:
- place = place name of the marker
- @location = panorama location
- data-location = place marker location
8. Web-API: Panorama preview
maps.googleapis.com/maps/api/streetview
? size = 640x360
& location = 48.8589363,2.295527
& fov = 130
& heading = 18
& pitch = 30
& key = API_KEY
maps.googleapis.com/maps/api/streetview
? size = 640x360
& pano = CAoSLEFGMVFpcE81WUZKcGNmeVFjX1FBa0dUUWJ6VVh3eUdZcDFXRnB6SF9mVmFw
& fov = 130
& heading = 18
& pitch = 30
& key = API_KEY
9. Web-API: Panorama metadata
maps.googleapis.com/maps/api/streetview/metadata
? location = 40.7143888,-74.0016005
& key=API_KEY
maps.googleapis.com/maps/api/streetview/metadata
? pano = CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5
& key=API_KEY
Result:
{
"copyright" : "© Matthew MacVey",
"date" : "2018-05",
"location" :
{
"lat" : 40.7143888,
"lng" : -74.0016005
},
"pano_id" : "CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5",
"status" : "OK"
}
10. Web-API: Place information
maps.googleapis.com/maps/api/place/textsearch/json
? query = Big-Ben
& key = API_KEY
Result:
{
"html_attributions": [],
"results": [
{
"business_status": "OPERATIONAL",
"formatted_address": "London SW1A 0AA, United Kingdom",
"geometry": {
"location": {
"lat": 51.50072919999999,
"lng": -0.1246254
},
"viewport": {
"northeast": {
"lat": 51.50211262989271,
"lng": -0.12381395
},
"southwest": {
"lat": 51.49941297010727,
"lng": -0.1269553499999999
}
}
},
"icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png",
"icon_background_color": "#7B9EB0",
"icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet",
"name": "Big Ben",
"photos": [
{
"height": 3970,
"html_attributions": [
"<a href=\"https://maps.google.com/maps/contrib/108563206122115577170\">Miguel Ángel Romacho Contreras</a>"
],
"photo_reference": "AUacShixy_iNsH-O1SgdK6sCUEpqgarAI8y-xpQ942CNSVrqddsB6cIyQ19VE6v6UijDZRptsWhZqA3IgQSyZ2ewIq2nxdtVhy2ywoE5FD7T6w_tP5_4T18UZKCZIR84F0bemWL_TZiSKyDUbzPnke_ciTSgH8EsyMV_8BLz6x2IpQ0iGoBO",
"width": 5955
}
],
"place_id": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",
"plus_code": {
"compound_code": "GV2G+74 London, United Kingdom",
"global_code": "9C3XGV2G+74"
},
"rating": 4.4,
"reference": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",
"types": [
"tourist_attraction",
"point_of_interest",
"establishment"
],
"user_ratings_total": 54585
}
],
"status": "OK"
}
11. Web-API: Sign your request
12. Android Place API
Places.initialize(appContext, mapsApiKey);
String placeId = "ChIJT9j2eKVv5kcRp9MrU92slfY";
final PlacesClient placesClient = Places.createClient(activityContext);
final List<Place.Field> fields = singletonList(Place.Field.LAT_LNG);
final FetchPlaceRequest request = FetchPlaceRequest.newInstance(placeId, fields);
placesClient.fetchPlace(request)
.addOnSuccessListener(response -> {
final Place place = response.getPlace();
final LatLng location = place.getLatLng());
}).addOnFailureListener(exception -> {
logger.error("Place not found:", exception);
});
13. Android Geocoder
final Geocoder geocoder = new Geocoder(appContext);
String searchString = "Big Ben";
final List<Address> addresses = geocoder.getFromLocationName(searchString, 1);
LatLng location = null;
if (null != addresses && !addresses.isEmpty()) {
final Address address = addresses.get(0);
location = new LatLng(address.getLatitude(), address.getLongitude());
}
14. CID Issue
Sometimes, Google Maps app generate unusual link with parameter "cid":
maps.google.com/?cid=13226830714359798441
CID (Customer ID Number) is a unique Google identifier of a business entity. In other words, Google uses this number to identify an account of a business and cluster business information across GMB, Ads, Analytics, Maps and other Google’s properties.
Using a webservice API, you can retrieve information about a place using the specified identifier:
maps.googleapis.com/maps/api/place/details/json
? cid = CID
& key = API_KEY
But this method is unsafe for use in an Android application
because all the keys are stored within the app, and theoretically, an
attacker could extract them from there. However, using a Maps API key
can be secured at the Google Console level by specifying 'Set an
application restriction' and 'Android Fingerprint restriction.'
Unfortunately, such restrictions cannot be applied to a key used for
retrieving information through a webservice, creating a theoretical
vulnerability for third-party use of this key.
However, there is another way to obtain the necessary information. It involves making a request to the provided link, parsing the HTML page of the response body, and extracting the metadata containing the place's name. From there, it's straightforward – using a geocoder, we can find the location.
Meta-information in the response html:
<meta content="Big Ben · London SW1A 0AA, United Kingdom"
itemprop="name">
<meta content="Big Ben · London SW1A 0AA, United Kingdom"
property="og:title">
<meta content="Big Ben · London SW1A 0AA, United Kingdom"
property="og:site_name">
Send request to CID-URL, receive response and extract meta from the body:
https://gist.github.com/asilichenko/b0000eb1562c9e4e75b0d43d799260bc
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class MetadataExtractor {
private static final String META_ITEMPROP_NAME =
"meta[itemprop=name]";
private static final String META_PROPERTY_OG_TITLE =
"meta[property=og:title]";
private static final String META_PROPERTY_OG_SITE_NAME =
"meta[property=og:site_name]";
private static final String CONTENT = "content";
private final OkHttpClient httpClient;
public MetadataExtractor() {
this.httpClient = new OkHttpClient();
}
public Metadata extract(String url) {
final Request request = new Request.Builder().url(url).build();
try (final Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("Request failed: " + response.code());
}
Metadata retval = null;
final ResponseBody body = response.body();
if (null != body) {
final String html = body.string();
final Document doc = Jsoup.parse(html);
final String name = getMetaContent(doc, META_ITEMPROP_NAME);
final String title = getMetaContent(doc, META_PROPERTY_OG_TITLE);
final String siteName = getMetaContent(doc, META_PROPERTY_OG_SITE_NAME);
retval = new Metadata(name, title, siteName);
}
return retval;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
/**
* Get an attribute value from the first matched element
* that has the attribute.
*/
private String getMetaContent(Document doc, String selector) {
return doc.select(selector).attr(CONTENT);
}
public static class Metadata {
public final String name;
public final String title;
public final String siteName;
public Metadata(String name, String title, String siteName) {
this.name = name;
this.title = title;
this.siteName = siteName;
}
}
}
15. Conclusion
In this article, I have presented various scenarios illustrating when a shared link on Google Maps can be generated.
Here are the conclusions I've drawn about the data in the links:
- If a place or a marker is selected, then locations in the path and in the data both point to the selected place/marker.
- Place name can be represented either as string or coordinates as pair of doubles or in DMS format.
- If panorama is opened then panorama ID is present.
- If a panorama is opened, then the @location equals the panorama's location; otherwise, the @location equals the map camera's location.
- If the opened panorama belongs to the user, then the ID is useless, but the @location points to the panorama's location.
- Mobile Google Maps currently does not support having both a selected marker and an opened panorama simultaneously. Therefore, all locations in the link are always consistent and point to user's desired location.
Now, let's explore the various types of Google Maps links I've
identified. I've simplified them by removing unnecessary details to
focus on the fundamental structure.
/maps/search/48.718116,+2.627758
/maps/place/
-13.165331,-72.548991/
data = !8m2 !3d -13.1653311 !4d -72.5489906
/maps/place/
Empire+State+Building/
@40.7321378,-73.9691401,17.87z/
data = !8m2 !3d 40.7484405 !4d -73.9856644
/maps/place/
Empire+State+Building/
@40.7413886,-73.9891371,3a,75y,230.15h,116.97t/
data = !1s nhbQjZVsxbtfxb9rTUhZmQ !2e0
!8m2 !3d 40.7484405 !4d -73.9856644
/maps/place/Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/
/maps/
@48.858345,2.2943299,0a,75y,312.57h,84.6t/
data = !1s _BgQhZS8mY-Zix5BdCt2mA !2e0
/maps?q=50.8136769,-2.4746919
/maps?q=London,+Great+Britain
/maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA
Finally, let's define the general scheme of link structure based on coordinates and panorama ID:
/maps/search/COORDINATES
/maps/search/STRING
===
/maps/place/NAME
/maps/place/NAME/
@COORDINATES_MAP/
data=COORDINATES_PLACE
/maps/place/NAME/
@COORDINATES_PANORAMA/
data=PANORAMA_ID
!COORDINATES_PLACE
/maps/place/
COORDINATES/
data=COORDINATES
===
/maps/@COORDINATES
/maps/@COORDINATES/data=PANORAMA_ID
===
/maps?q=COORDINATES
/maps?q=STRING
===
maps.google.com/?cid=CID
/maps?cid=CID
Thanks for your time and best of luck with your mapping and panoramic adventures!