Skip to Content
Author's profile photo Alexey Ledenev

Google geocoding API for address check and normalization

SAP Hybris Cloud for Sales and Service has a feature to check postal address format, but has no OTB-feature to correct mistakes/misprints as well as the system has no feature for automatic determination of PO Box number based on city, street names and house number. Geo-coordinates of the address are also desirable to be determined. External geocoding service can help us. The following demo prototype shows how to use Google Geocoding API service in SAP Hybris Sales&Service .

I’m using Google Geocoding API (https://developers.google.com/maps/documentation/geocoding/intro?hl=en#language-param) but this example can be easily adapted for any REST-based geocoding service.

I also used SCN article «How to Parse a String in JSON Format in ABSL» (https://blogs.sap.com/2013/10/07/how-to-parse-a-string-in-json-format-in-absl/) by Thomas Schneider and priceless answer about URL encoding from Stefan Hagen in this topic https://answers.sap.com/questions/67032/url-encode.html.
Sincere thanks!

Step 1: Getting API-key.

First, you have to register as a developer in google and get API key. Here is a guide. It’s free of charge and you can send up to 2500 requests to Google daily. https://developers.google.com/maps/documentation/geocoding/get-api-key?hl=en

It is free of charge and you can send up to 2500 requests to Google daily (https://developers.google.com/maps/documentation/geocoding/usage-limits).

Step 2: Testing API via browser.

After you got the key and activated geocoding service it’s time to check the APi via your web-browser. My example is below, the address is written in Russian with misprints and mistakes. The API corrects errors and normalizes the form of address. Additionally it returns the result on any language you need (lang=EN parameter of the url).

Step 3: Service Configuration.

Create a solution and add Google geocoding service to it.

Choose REST Service

Add an URL https://maps.googleapis.com/maps/api/geocode/json

Choose «Create Communication Scenario» and define API key “key”

Click Next, then Finish and activate newly created service and communication scenario. In some cases you cannot add the API key name when you create he service – just finish the process, activate the service and add key name after that. Don’t forget to re-activate the service.

Then right click on the newly created scenario, choose «Manage Communication Arrangement» and create the new one based on arrangement «GOOGLE» we created before.

Define System Instance ID

Choose the Authentication method NONE and Enter the API Key you have got from Google.

Save and activate everything.

Step 4: Service Configuration.

 

As I told before, I am using some code examples from SCN with adaptation to my case. The big challenge here is that an Address BO is not available for extending. So, I used Customer BO and worked with default customer address in action Customer->After-Modify.

Create custom BO for JSON parser

import AP.Common.GDT as apCommonGDT;

businessobject JsonStructure_BO {

	element ID:ID;

	node Result [0,n] 
		{
			element ID : IntegerValue;
			element ParentID : IntegerValue;
			element NodeLevelID: IntegerValue;
			element Name : LANGUAGEINDEPENDENT_EXTENDED_Name;
			element Value : LANGUAGEINDEPENDENT_EXTENDED_Name;
		}
}

 

Add the code to Customer-AfterModify action.

The following code is the demo example, so, it must be optimized before using in production
Part 1: Running the service

import ABSL;
import AP.PlatinumEngineering;
import AP.Common.GDT;

// Communication details
var ScenarioName = "GOOGLE";
var ServiceName = "GOOGLE";
var HttpMethod = "GET";
var HttpResource = ""; //URL – File Name
// not required for this example
var ContentType = "";
var Body = "";
var HeaderParameter : collectionof NameAndValue; // Set URL Parameter


var URLParameter : collectionof NameAndValue;
var URLParameterEntry : NameAndValue;
URLParameterEntry.Name = "address";

if (this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.IsSet()) // to check the adress is entered
{
	var originalCityName = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.CityName;
	var originalStreetName = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.StreetName;
	var originalHouseNumber = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.HouseID;
	URLParameterEntry.Value = URL.Encode(originalCityName.Concatenate(",".Concatenate(originalStreetName.Concatenate(",".Concatenate(originalHouseNumber))))); // address string to check via Google
}
else // nothing to normalize
{
	URLParameterEntry.Value = "";
}
URLParameter.Add(URLParameterEntry);

URLParameterEntry.Name = "language";
URLParameterEntry.Value = Context.GetCurrentUserLanguage().ToString();
URLParameter.Add(URLParameterEntry);

// Execute webservice call
var ws_result = WebServiceUtilities.ExecuteRESTService(ScenarioName, ServiceName, HttpMethod, HttpResource, URLParameter, HeaderParameter, ContentType, Body);

Part2: Parsing JSON answer.  I used SNC example (https://blogs.sap.com/2013/10/07/how-to-parse-a-string-in-json-format-in-absl/) with little modification

//JSON Parser;
var json = ws_result.Content;
var i = 0;
var i0 = 0;
var controlTag = ""; 
var controlTag2;
var lastControlTag;
var noOpenObjects = 0; // for checking check only
var noOpenArrays = 0; // for checking check only
var afterColon = false;
var string;
var result : elementsof JsonStructure_BO.Result;
var results : collectionof elementsof JsonStructure_BO.Result;
var ID = 0;
var parentID = 0;
var errorFlag = 0;
var blockId = 0;

if (ws_result.Code.Replace(" ","")=="200") // do only if return code is 200 ("OK")
{
while (i<json.Length())
{
 i = json.FindRegex("\\{|\\}|\\[|\\]|\"|:|,|\\d+|true|false", i);
 if (i < 0) {
  //raise Message.Create("E", "Parsing error (control tag not found)"); 
  break; // *** Error ***
 }
 lastControlTag = controlTag;
 controlTag = json.Substring(i,1);
 switch (controlTag) {
 case "{" , "}", "[", "]", ","  {
  i = i + 1;
  switch (controlTag) {
  case "{" {
   noOpenObjects = noOpenObjects + 1;
   parentID = ID;
   blockId = blockId+1;
  }
  case "}" {
   noOpenObjects = noOpenObjects - 1;
  // lookup for "grandparent ID" -> parentID
   if (!(parentID == 0)) {
    var partentIDtab = results.Where(n=>n.ID == parentID);
    if (partentIDtab.Count() == 1) {
     parentID = partentIDtab.GetFirst().ParentID;
    }
    else {
     //raise Message.Create("E", "Parsing error (parentID not found)"); 
	 errorFlag = 1;
	 break; // *** Error ***
    }
   }
  }
  case "[" {
   noOpenArrays = noOpenArrays + 1;
   parentID = ID;
  }
  case "]" {
   noOpenArrays = noOpenArrays - 1;
  // lookup for "grandparent ID" -> parentID
   if (!(parentID == 0)) {
    var partentIDtab = results.Where(n=>n.ID == parentID);
    if (partentIDtab.Count() == 1) {
     parentID = partentIDtab.GetFirst().ParentID;
    }
    else {
     //raise Message.Create("E", "Parsing error (parentID not found for array)"); 
	 errorFlag = 1; break; // *** Error ***
    }
   }
  }
  } 
  // close old node and open new one with exception list
  if ( (controlTag == "," && (lastControlTag == "}" || lastControlTag == "]")) ) {
   // no new node required
  }
  else {
   if( ID > 0 ) {results.Add(result);}
   ID = ID + 1;
   result.ID = ID;
   result.ParentID = parentID;
   result.Name = "";
   result.Value = "";
   result.NodeLevelID =  blockId;
  }
  afterColon = false;
 }  // end case "{" , ",", "}"
 case ":" {
  if (afterColon) {
   //raise Message.Create("E", "Parsing error (open colon)"); 
   errorFlag = 1; break; // *** Error ***
  }
  afterColon = true;
  i = i + 1;
 }
 case "\"" {
  i = i + 1;
  i0 = i;
  i = json.FindRegex("\\{|\\}|\\[|\\]|\"", i);
  if (i < 0) {
   //raise Message.Create("E", "Parsing error (control tag not found after quote)"); 
   errorFlag = 1; break; // *** Error ***
  }
  controlTag2 = json.Substring(i,1);
  if (controlTag2 == "\"") {
   string = json.Substring(i0, i-i0);
   if (!afterColon) {   // before colon -> name
    result.Name = string;
   }
   else {// after colon -> value
    result.Value = string;
    afterColon = false;
   }
  }
  else {
   //raise Message.Create("E", "Parsing error (quote not closed)"); 
   errorFlag = 1; break; // *** Error ***
  }
  i = i + 1;
 }
 case "t", "T" {
  if (!afterColon) {
   //raise Message.Create("E", "Parsing error (true/false)"); 
   errorFlag = 1; break; // *** Error ***
  }
  result.Value = "true";
  afterColon = false;
  i = i + 4;
 }
 case "f", "F" {
  if (!afterColon) {
   //raise Message.Create("E", "Parsing error (true/false)"); 
   errorFlag = 1; break; // *** Error ***
  }
  result.Value = "false";
  afterColon = false;
  i = i + 5;
 }
 default {
  // implementation for numeric (\d in regex)
  if (!afterColon) {
   //raise Message.Create("E", "Parsing error (numeric before colon)"); 
   errorFlag = 1; break; // *** Error ***
  }
  i = i + 1;
  i0 = i - 1;
  i = json.FindRegex("\\{|\\}|\\[|\\]|\"|:|,|true|false", i);
  if (i < 0) {
   //raise Message.Create("E", "Parsing error (numeric: end not found)"); 
   errorFlag = 1; break; // *** Error ***
  }
  controlTag2 = json.Substring(i,1);
  if (controlTag2 == "}" || controlTag2 == ",") {
   string = json.Substring(i0, i-i0);
   result.Value = string;
   afterColon = false;
  }
  else {
   //raise Message.Create("E", "Parsing error (numeric end not correct)"); 
   errorFlag = 1; break; // *** Error ***
  }
 }
 }

 // check
 if (noOpenObjects < 0 || noOpenArrays < 0) 
 {
  //raise Message.Create("E", "Parsing error (error during object/array processing) (1)");  // *** Error ***
  errorFlag = 1;
 }

} //while end


// final check
if (!(noOpenObjects == 0) && !(noOpenArrays == 0) && !(parentID == 0)) 
{
 //raise Message.Create("E", "Parsing error (error during final check"); // *** Error ***
 errorFlag = 1;
}
// End of JSON Parser

 

Part 3: Address data normalization. (for production purposes you have to add exception processing). Note, that different countries have different address formats and GOOGLE API returns the address a little different depending on the country. C4C has universal data storage format, so modify your code for country specific form. The code below is relevant for Russia and CIS countries.

var resultTab: collectionof elementsof JsonStructure_BO.Result;
var postalCode = "";
var streetName = "";
var townName = "";
var countryCode = "";
var houseNumber = "";
var lat = 0;
var long = 0;
var coordinates: GeoCoordinates;

if (this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.IsSet()) // to check that an address BO exists
{
foreach (var r in results)
{
	switch (r.Name)
	{
		case "postal_code"
		{
			resultTab = results.Where(n=>n.NodeLevelID == r.NodeLevelID && n.Name == "short_name");
			if (!resultTab.GetFirst().IsInitial()) 
				{
					postalCode = resultTab.GetFirst().Value;
					if (postalCode.Length()>0)
					{
						this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.StreetPostalCode = postalCode;
							
					}
				}
		}
		case "route" // street name
		{
			resultTab = results.Where(n=>n.NodeLevelID == r.NodeLevelID && n.Name == "short_name");
			if (!resultTab.GetFirst().IsInitial()) 
				{
					streetName = resultTab.GetFirst().Value;
					if (streetName.Length()>0)
					{
						this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.StreetName = streetName;
							
					}
				}
		}
		case "administrative_area_level_2" // town name 
		{
			resultTab = results.Where(n=>n.NodeLevelID == r.NodeLevelID && n.Name == "short_name");
			if (!resultTab.GetFirst().IsInitial()) 
				{
					townName = resultTab.GetFirst().Value;
					if (townName.Length()>0)
					{
						this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.CityName = townName;
							
					}
				}
		}
		case "country" // country code
		{
			resultTab = results.Where(n=>n.NodeLevelID == r.NodeLevelID && n.Name == "short_name");
			if (!resultTab.GetFirst().IsInitial()) 
				{
					countryCode = resultTab.GetFirst().Value;
					if (countryCode.Length()>0)
					{
						this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.CountryCode = countryCode;
							
					}
				}
		}
		case "street_number" //house number from Google...
		{
			resultTab = results.Where(n=>n.NodeLevelID == r.NodeLevelID && n.Name == "short_name");
			if (!resultTab.GetFirst().IsInitial()) 
				{
					houseNumber = resultTab.GetFirst().Value;
					if (houseNumber.Length()>0)
					{
						this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.HouseID = houseNumber;
					}
				}
			
		}
		case "location" // geolocation coordinates
		{
			resultTab = results.Where(n=>n.ParentID == r.ID);
			if (resultTab.Count()>0)
			{
				foreach (var rr in resultTab)
				{
					switch (rr.Name)
					{ 
						case "lat"
						{
						coordinates.LatitudeMeasure.content = Numeric.ParseFromString(rr.Value.Replace("\n","").Trim());
						//this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LatitudeMeasure.content = Numeric.ParseFromString(rr.Value);
						}
						case "lng"
						{
						//Trace.Info("A STR=",rr.Value);
						//Trace.Info("B STR=",rr.Value.Replace("\n","").Trim());
						coordinates.LongitudeMeasure.content = Numeric.ParseFromString(rr.Value.Replace("\n","").Trim());
						//this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LongitudeMeasure.content = Numeric.ParseFromString(rr.Value);
						}
					}					
				}
				if (this.CurrentDefaultAddressInformation.Address.GeographicalLocation.IsSet())
				{
					this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LongitudeMeasure = coordinates.LongitudeMeasure;
					this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LatitudeMeasure = coordinates.LatitudeMeasure;
				}
				else
				{
					this.CurrentDefaultAddressInformation.Address.GeographicalLocation.Create();
					this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LongitudeMeasure = coordinates.LongitudeMeasure;
					this.CurrentDefaultAddressInformation.Address.GeographicalLocation.GeoCoordinates.LatitudeMeasure = coordinates.LatitudeMeasure;
				}
				
			}
		}
	} // end switch

}//end foraech

}// end if

} // end "global" if

 

 

Step 5: Testing.

Here is the Customer address before normalization

The same object after – the address is in normal form (to be written by Russian speaking postal worker :)), PO box number is automatically added and geo coordinates are also determined.

 

I hope you have found some interesting ideas here :). Will appreciate any comments and new ideas!

Regards,
Alexey

Assigned Tags

      24 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Andrei Vishnevsky
      Andrei Vishnevsky

      Hi Alexey,

      Great blog I need to admit, thanks 🙂
      One question: is there a possibility to raise a popup to choose one of addresses proposed by Google?

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Hi Andrey,

      I did not try this. But I'm affraid this is not easy "walk" on standart screens. 🙁

      Alexey

      Author's profile photo Andrei Vishnevsky
      Andrei Vishnevsky

      And another question if you don't mind. You used JSON to BO conversion. But what about the opposite way? Is there any quick way to do BO to JSON conversion?

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      What is your task? There is otb-feature for simple odata services. You can read/write BO with xml or json based web requests.

      Author's profile photo Andrei Vishnevsky
      Andrei Vishnevsky

      Hi Alexey,

      My question is not directly related to the topic of your blog post. However, as soon as you’ve touched json-to-bo conversion I decided you might have an idea on my question as well.

      Let’s pretend that I need to send a custom BO data as a body of POST/PUT method. I need to compose JSON from this BO’s fields. Right now I can do that (at least I know only this way) doing a “concatenate”. Means when there are many fields in BO there will be too much code to write down and maintain in the future.

      So my question is: do you know any way to easily produce JSON string from particular BO instance?

      I know that there is such a feature as “External OData Services Consumption” . Unfortunately this feature supports only READ operation and function imports. But no CREATE/UPDATE. At least that I found out from the documentation.

      Not sure that I understood what you meant under “otb-feature” in this terms.

      Author's profile photo Johannes Schneider
      Johannes Schneider

      Hi Alexey,

      I have a question regarding the API keys. I maintainend 2 keys for my scenario and maintained them in the communication arrangement but they do not appear in the URL in my script. The URL I'm watching at the is from the ws_result after it got executed.

      Kind Regards,
      Johannes

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Hi Johannes,

      Regarding URL in ws_result structure I have the same situation - no API key paramenters appear. It seems like the way how the framwork works.

      Regards,
      Alexey

      Author's profile photo Johannes Schneider
      Johannes Schneider

      Hi Alexey,

      is there a different way to build up the URL where any customer could set an ID and pw which then will be used in the URL. Something like www.loginpage.com/id=replace1&pw=replace2. Of course the customer wants to handle it easy and does have many different accounts on this website. Is there a proper standard way because at the moment (for testing purposes) i maintain it hard coded in a script. Sorry for not replying to your answer but there is no other option then to like your answer.

      Kind Regards,
      Johannes
       

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Hi Johannes,

      You can create code list datatype as a part of business configuration and then the customer can handle it via fine tuning (as described here starting from the page 305 http://help.sap.com/saphelpiis_studio_1611/studio_od_1611.pdf ). This codelist is avalable in ABSL script. Or you can create custom BO where all the connection parameters to different acconts are stored.

      Regards

      Alexey

      Author's profile photo Johannes Schneider
      Johannes Schneider

      Hi Alexey,

      Thanks for the hint with the Business Configuration Object.

      Thats what i needed 🙂

       

      Kind Regards,

      Johannes

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      in addition to the last answer - or you can store login and pw as additional fields on user's personal file (Employee BO). The final way where to store login and pw depends on exact business scenario.

      Author's profile photo Former Member
      Former Member

      Great blog. Thanks Alexey. By the way, can you please share if you have tried a similar call via ABAP? Am looking for a solution to find the shortest route between two addresses in an ABAP program.

       

      Thanks

      Sudarshan

      Author's profile photo Andrei Vishnevsky
      Andrei Vishnevsky

      Hi Sudarshan,

       

      For ABAP please take a look at project ZGEOCODE. Having geocodes you can use some services to further obtain/calculate the best route. For example Google's Distance Matrix API

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      Hi  Alexey,

      I tried to do the same as explained in the blog and i did not get the expected Result. I checked in Debug mode as well. It seems that Web-service doesn’t get executed as Request URL is not created.

      I am sure the API key is working fine. I checked in Browser.

       

      Communication arrangement : Google

      I am not sure what I am doing wrong.

      Can you please guide?

       

      Thank you

      Regards

      Prat

       

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Hi Prat,

       

      Did you trace this part of the code? Does it work correct?

      if (this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.IsSet()) // to check the adress is entered

      {

      var originalCityName = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.CityName;

      var originalStreetName = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.StreetName;

      var originalHouseNumber = this.CurrentDefaultAddressInformation.Address.DefaultPostalAddressRepresentation.HouseID;

      URLParameterEntry.Value =

      URL.Encode(originalCityName.Concatenate(",".Concatenate(originalStreetName.Concatenate(",".Concatenate(originalHouseNumber))))); // address string to check via Google

      }

      else // nothing to normalize

      { URLParameterEntry.Value = ""; }

      URLParameter.Add(URLParameterEntry);

      URLParameterEntry.Name = "language";

      URLParameterEntry.Value = Context.GetCurrentUserLanguage().ToString();

      URLParameter.Add(URLParameterEntry);

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      Hi Alexey,

       

      Yes  I debugged this as well. You can see the value in the same screen shot above in Local Variable : URL Parameter.

       

      Regards

      Prat

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Did you check also this coma in url parameters? Underlined in red below.

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      The comma is due to following code as mentioned in your code.

      URLParameterEntry.Value = URL.Encode(originalCityName.Concatenate(",".Concatenate(originalStreetName.Concatenate(",".Concatenate(originalHouseNumber))))); // address string to check via Google
      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      yes, that's true.. 🙂

      So,  the only diffenrence I can see  - the parameter language. But as I know it's not important. See my trace log below.

      What realy confuses me - you do not have an error message from the service. Did you try to re-activate the solution?  Looks like something wrong with communication system configuration.

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      Yes I Reactivated my solution.

      I am attaching  Screen shot of communication system

       

      Also I raised incident , See the response of SAP after 4 days 🙂 .

      I have to work on a PoC for a prospect , If you can help more.

       

      Thank you

      Regards

      Prat

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      Also In Communication Arrangement : Ping fails.

       

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Hi

      I've send you a personal message

      Author's profile photo Alexey Ledenev
      Alexey Ledenev
      Blog Post Author

      Doublecheck the scenario name - it's case sencitive

      var ScenarioName = "Google";  as not the same as var ScenarioName = "GOOGLE"

       

      Author's profile photo Pratyush SINHA
      Pratyush SINHA

      Hello Alexey,

      Parser doesn't work when there is negative symbol in Lng

      "location" : {
                     "lat" : 47.211186,
                     "lng" : -1.6137434
                  },
      
      Address Example  :

      Street 5 RUE DE SAINT-NAZAIRE ,

      Postal Code : 44800 ,

      City :  SAINT-HERBLAIN

      Country : France

      Can you please Help with this.

      Regards

      Prat