How to receive shared text in the Android App

First of all, we need to register our app as the plain text receiver by adding the intent filter into the Manifest for a certain activity:

<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
</intent-filter>

The standard way to receive shared text is by calling the getStringExtra method for the intent:

final Intent intent = getIntent();
final String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);

It is also useful to preliminarily check the intent action:

final Intent intent = getIntent();
if (Intent.ACTION_SEND.equals(action)) {
  ...
}

This code can be called either from the onCreate method if we expect to receive data when the app is started, or from the onResume method if the app has already started and is in the background.

However, there is an issue when the app is called from the background - sometimes we receive the android.intent.action.MAIN action instead of SEND, and the extra is empty. But why does this happen? When does "sometimes" occur? Let's explore this issue.

Test case #1

  1. Our app is shut down
  2. Open Web-browser
  3. Share some link to the app
  4. The app is opened
  5. Method onCreate is called
  6. Shared text successfully received
  7. Switch back to the Web-browser
  8. Share link again
  9. The onResume method is called this time, and the data can be received.

Android activity lifecycle: Test case #1
 Nothing odd so far. Let's take a look Test case #2:

Test case #2

  1. Our app is shut down
  2. Start the app
  3. Hide the app into background
  4. Open the Web-browser
  5. Share link to the app
  6. The onResume method is called, but this time we got no data

Android activity lifecycle: Test case #2

Let's look into the logs, there we can see a pair of interesting records:

ActivityTaskManager I START 
  u0 {act = android.intent.action.SEND
    typ = text/plain
    flg = 0x13080001 
    cmp = ua.in.asilichenko.sharedtextreceiver/.MainActivity 
    clip = {text/plain {T(475)}}
    (has extras)
  } from uid 10191

ActivityTaskManager I Launching r: 
  ActivityRecord {
    90f60f2
    u0
    ua.in.asilichenko.sharedtextreceiver/.MainActivity
  }
  from background: 
    ActivityRecord {
      90e8155
      u0
      com.android.chrome/com.google.android.apps.chrome.Main
    } 
    t24314}. 
    New task: false

The conclusions that can be drawn from this log are:

  • Action android.intent.action.SEND with some data was sent indeed, but the app did not receive it
  • There is some flag: 0x13080001
  • The ActivityTaskManager determined that the app had already run, so it didn't start a new instance but restored the one from the background

The solution to this problem lies in overriding the onNewIntent method:

This is called for activities that set launchMode to "singleTop" in their package, or if a client used the Intent.FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity. In either case, when the activity is re-launched while at the top of the activity stack instead of a new instance of the activity being started, onNewIntent() will be called on the existing instance with the Intent that was used to re-launch it.
An activity can never receive a new intent in the resumed state. You can count on onResume being called after this method, though not necessarily immediately after the completion this callback. If the activity was resumed, it will be paused and new intent will be delivered, followed by onResume. If the activity wasn't in the resumed state, then new intent can be delivered immediately, with onResume() called sometime later when activity becomes active again.
Note that getIntent still returns the original Intent. You can use setIntent to update it to this new Intent. Dispatches this call to all listeners added via addOnNewIntentListener(Consumer). Handle onNewIntent() to inform the fragment manager that the state is not saved. If you are handling new intents and may be making changes to the fragment state, you want to be sure to call through to the super-class here first. Otherwise, if your state is saved but the activity is not stopped, you could get an onNewIntent() call which happens before onResume() and trying to perform fragment operations at that point will throw IllegalStateException because the fragment manager thinks the state is still saved.

So it turns out that if the sending application launches our application, the data will continue to be successfully received through the intent. However, in the case where the application was launched separately from the sender, a new intent will be created each time, and the sent data will not be transferred to the new intent. Nevertheless, the original intent from the sender can be obtained in the onNewIntent method.

@Override
protected void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  setIntent(intent);
  receiveText();
}
Android activity lifecycle: Test case #2. Fix

Now, both test cases works fine, but I found another bug.

Test case #3

  1. Our app is in the background
  2. Open Web-browser
  3. Share a link
  4. The app is opened
  5. Method onCreate is called

Android activity lifecycle: Test case #3

Now, if we look in the background stack, we can see two instances of our app, and that's not good.

To fix this, we need to set launchMode for the main activity to "singleTask":

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:launchMode="singleTask">

In this mode, if there is already an existing instance of the activity in the system, a new one will not be created, and the existing one will be called. 

Android activity lifecycle: Test case #3. Fix

Let's check one more case.

Test case #4

  1. Share data into our app
  2. Data is received
  3. Rotate the device
  4. The same data is received again

This happens because when the Android device is rotated, it recreates the activity, but the intent remains the same. As a result, the processing of shared text in the onCreate method is triggered again, and the read data is also re-read.

Android activity lifecycle: Test case #4

To avoid re-reading the data, you need to remove them from the intent using the removeExtra method:

if (Intent.ACTION_SEND.equals(action)) {
  final String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
  if (null != stringExtra) {
    ...
    intent.removeExtra(Intent.EXTRA_TEXT);
  }
}

Android activity lifecycle: Test case #4. Fix

That's all for today.

Unlocking Google Street View: Extracting Panorama ID and Location from the Google Maps Link

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:

  1. Point
  2. Place
  3. Panorama
  4. User panorama
  5. Search for a location, place or address
  6. Special case #1: Place + Map moved
  7. Special case #2: Place + Panorama

Other sections:

  1. Web-API: Panorama preview
  2. Web-API: Panorama metadata
  3. Web-API: Place information
  4. Web-API: Sign your request
  5. Android Place API
  6. Android Geocoder
  7. CID issue
  8. 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.

  • Location
www.google.com/maps?q=50.813588,-2.475488
  • Place
www.google.com/maps?q=London,+Great+Britain
  • Address
www.google.com/maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA

6. Special case #1: Place + Map moved

  1. Click on any place-marker.
  2. Move map camera away.
  3. 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
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7321378,-73.9691401 = 40°43'55.7"N, 73°58'08.9"W - East River
  • data-location:
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

  1. Select any place marker.
  2. Turn on Street View layer.
  3. Move map camera away.
  4. Open panorama on any place, not the original place.
  5. 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
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7413886,-73.9891371 = 40°44'29.0"N, 73°59'20.9"W - Flatiron Building
  • data-location:
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

  • by location:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & location = 48.8589363,2.295527 & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY
  • by ID:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & pano = CAoSLEFGMVFpcE81WUZKcGNmeVFjX1FBa0dUUWJ6VVh3eUdZcDFXRnB6SF9mVmFw & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY

9. Web-API: Panorama metadata

  • by location:
maps.googleapis.com/maps/api/streetview/metadata ? location = 40.7143888,-74.0016005 & key=API_KEY
  • by ID:
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.

  • point -> web:
/maps/search/48.718116,+2.627758
  • point -> app:
/maps/place/ -13.165331,-72.548991/ data = !8m2 !3d -13.1653311 !4d -72.5489906
  • place -> web:
/maps/place/ Empire+State+Building/ @40.7321378,-73.9691401,17.87z/ data = !8m2 !3d 40.7484405 !4d -73.9856644
  • place + panorama:
/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
  • place -> app:
/maps/place/Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/
  • panorama:
/maps/ @48.858345,2.2943299,0a,75y,312.57h,84.6t/ data = !1s _BgQhZS8mY-Zix5BdCt2mA !2e0
  • query:
/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!