[
  {
    "path": "Cleanup.gs",
    "content": "// Geocode Addresses: Remove User Data\n// Copyright (c) 2021 Max Vilimpoc\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n// THE SOFTWARE.\n\n// Remove all user-created sheets.\n//\n// Remove all user-created images.\n//\n// Remove all user-entered data from unprotected ranges on a specific sheet\n// using an App Script installable trigger.\n\nfunction cleanupEverything() {\n  let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();\n  let sheets      = spreadsheet.getSheets();\n\n  Logger.log(\"Clearing user-created Sheets.\");\n\n  for (let s of sheets) {\n    // Do not delete _these_ sheets.\n    if (s.getName() === 'Test Addresses' || s.getName() === 'Reverse To Components' || s.getName() === 'Mapping') continue;\n\n    Logger.log(`Removing \"${s.getName()}\".`);\n    spreadsheet.deleteSheet(s);\n  }\n\n  Logger.log(\"Clearing user-created Images.\");\n\n  let allImages = spreadsheet.getSheetByName('Mapping').getImages();\n\n  for (let i of allImages) {\n    Logger.log(`Removing \"${i}\".`);\n    i.remove();\n  }\n\n  Logger.log(\"Clearing user data from protected ranges.\");\n\n  spreadsheet.getRange(\"Test Addresses!F3:H22\").clear();\n  spreadsheet.getRange(\"Test Addresses!F3:G22\").setHorizontalAlignment('normal');\n  spreadsheet.getRange(\"Test Addresses!H3:H22\").setHorizontalAlignment('left');\n  \n  spreadsheet.getRange(\"Test Addresses!A24:H1000\").clear();\n  spreadsheet.getRange(\"Test Addresses!A24:H1000\").setHorizontalAlignment('left');\n  spreadsheet.getRange(\"Test Addresses!F24:G1000\").setHorizontalAlignment('normal');\n  \n  spreadsheet.getRange(\"Reverse To Components!D3:L9\").clear();\n  spreadsheet.getRange(\"Reverse To Components!D3:L9\").setHorizontalAlignment('left');\n\n  spreadsheet.getRange(\"Reverse To Components!A11:L1000\").clear();\n  spreadsheet.getRange(\"Reverse To Components!A11:L1000\").setHorizontalAlignment('left');\n  spreadsheet.getRange(\"Reverse To Components!B11:C1000\").setHorizontalAlignment('normal');\n\n  spreadsheet.getRange(\"Mapping!A3:Z1000\").clear();\n  spreadsheet.getRange(\"Mapping!A3:Z1000\").setHorizontalAlignment('normal');\n\n  Logger.log(\"Cleared.\");\n}\n"
  },
  {
    "path": "Code.gs",
    "content": "// Geocode Addresses\n// Copyright (c) 2016 - 2021 Max Vilimpoc\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n// THE SOFTWARE.\n\n// Maps Premium Plan Keys (Your Own)\n//\n// \"Enables the use of an externally established Google Maps APIs Premium Plan \n// account, to leverage additional quota allowances. Your client ID and signing\n// key can be obtained from the Google Enterprise Support Portal.\"\n//\n// https://developers.google.com/apps-script/reference/maps/maps#setAuthentication(String,String)\n//\n// If you have this information and want to use it to increase your geocoding \n// quota, enter it here as a string.\n\nvar mapsClientId   = null; // something like 'gme-123456789'\nvar mapsSigningKey = null; // something like 'VhSEZvOXVSdnlxTnpJcUE'\n\n// Bias the geocoding results in favor of these geographic regions.\n// The regions are specified as ccTLD codes.\n// \n// See: https://en.wikipedia.org/wiki/Country_code_top-level_domain\n//\n// Used:\n// https://mbrownnyc.wordpress.com/misc/iso-3166-cctld-csv/\n// http://www.convertcsv.com/csv-to-json.htm\n// to generate the functions for menu item handling.\n/*\nvar REGIONS = {\n  \"Afghanistan\": \"af\",\n  \"Aland Islands\": \"ax\",\n  \"Albania\": \"al\",\n  \"Algeria\": \"dz\",\n  \"American Samoa\": \"as\",\n  \"Andorra\": \"ad\",\n  \"Angola\": \"ao\",\n  \"Anguilla\": \"ai\",\n  \"Antarctica\": \"aq\",\n  \"Antigua and Barbuda\": \"ag\",\n  \"Argentina\": \"ar\",\n  \"Armenia\": \"am\",\n  \"Aruba\": \"aw\",\n  \"Ascension Island\": \"ac\",\n  \"Australia\": \"au\",\n  \"Austria\": \"at\",\n  \"Azerbaijan\": \"az\",\n  \"Bahamas\": \"bs\",\n  \"Bahrain\": \"bh\",\n  \"Bangladesh\": \"bd\",\n  \"Barbados\": \"bb\",\n  \"Belarus\": \"by\",\n  \"Belgium\": \"be\",\n  \"Belize\": \"bz\",\n  \"Benin\": \"bj\",\n  \"Bermuda\": \"bm\",\n  \"Bhutan\": \"bt\",\n  \"Bolivia\": \"bo\",\n  \"Bosnia and Herzegovina\": \"ba\",\n  \"Botswana\": \"bw\",\n  \"Bouvet Island\": \"bv\",\n  \"Brazil\": \"br\",\n  \"British Indian Ocean Territory\": \"io\",\n  \"Brunei Darussalam\": \"bn\",\n  \"Bulgaria\": \"bg\",\n  \"Burkina Faso\": \"bf\",\n  \"Burundi\": \"bi\",\n  \"Cambodia\": \"kh\",\n  \"Cameroon\": \"cm\",\n  \"Canada\": \"ca\",\n  \"Cape Verde\": \"cv\",\n  \"Cayman Islands\": \"ky\",\n  \"Central African Republic\": \"cf\",\n  \"Chad\": \"td\",\n  \"Chile\": \"cl\",\n  \"China\": \"cn\",\n  \"Christmas Island\": \"cx\",\n  \"Cocos (Keeling) Islands\": \"cc\",\n  \"Colombia\": \"co\",\n  \"Comoros\": \"km\",\n  \"Congo\": \"cg\",\n  \"Cook Islands\": \"ck\",\n  \"Costa Rica\": \"cr\",\n  \"Cote d'Ivoire\": \"ci\",\n  \"Croatia\": \"hr\",\n  \"Cuba\": \"cu\",\n  \"Cyprus\": \"cy\",\n  \"Czech Republic\": \"cz\",\n  \"Democratic People's Republic of Korea (North Korea)\": \"kp\",\n  \"Denmark\": \"dk\",\n  \"Djibouti\": \"dj\",\n  \"Dominica\": \"dm\",\n  \"Dominican Republic\": \"do\",\n  \"Ecuador\": \"ec\",\n  \"Egypt\": \"eg\",\n  \"El Salvador\": \"sv\",\n  \"Equatorial Guinea\": \"gq\",\n  \"Eritrea\": \"er\",\n  \"Estonia\": \"ee\",\n  \"Ethiopia\": \"et\",\n  \"European Union\": \"eu\",\n  \"Falkland Islands (Malvinas)\": \"fk\",\n  \"Faroe Islands\": \"fo\",\n  \"Federated States of Micronesia\": \"fm\",\n  \"Fiji\": \"fj\",\n  \"Finland\": \"fi\",\n  \"France\": \"fr\",\n  \"French Guiana\": \"gf\",\n  \"French Polynesia\": \"pf\",\n  \"French Southern Territories\": \"tf\",\n  \"Gabon\": \"ga\",\n  \"Gambia\": \"gm\",\n  \"Georgia\": \"ge\",\n  \"Germany\": \"de\",\n  \"Ghana\": \"gh\",\n  \"Gibraltar\": \"gi\",\n  \"Greece\": \"gr\",\n  \"Greenland\": \"gl\",\n  \"Grenada\": \"gd\",\n  \"Guadeloupe\": \"gp\",\n  \"Guam\": \"gu\",\n  \"Guatemala\": \"gt\",\n  \"Guernsey\": \"gg\",\n  \"Guinea\": \"gn\",\n  \"Guinea-Bissau\": \"gw\",\n  \"Guyana\": \"gy\",\n  \"Haiti\": \"ht\",\n  \"Heard Island and McDonald Islands\": \"hm\",\n  \"Holy See (Vatican City State)\": \"va\",\n  \"Honduras\": \"hn\",\n  \"Hong Kong\": \"hk\",\n  \"Hungary\": \"hu\",\n  \"Iceland\": \"is\",\n  \"India\": \"in\",\n  \"Indonesia\": \"id\",\n  \"Iraq\": \"iq\",\n  \"Ireland\": \"ie\",\n  \"Islamic Republic of Iran\": \"ir\",\n  \"Isle of Man\": \"im\",\n  \"Israel\": \"il\",\n  \"Italy\": \"it\",\n  \"Jamaica\": \"jm\",\n  \"Japan\": \"jp\",\n  \"Jersey\": \"je\",\n  \"Jordan\": \"jo\",\n  \"Kazakhstan\": \"kz\",\n  \"Kenya\": \"ke\",\n  \"Kiribati\": \"ki\",\n  \"Kuwait\": \"kw\",\n  \"Kyrgyzstan\": \"kg\",\n  \"Lao People's Democratic Republic\": \"la\",\n  \"Latvia\": \"lv\",\n  \"Lebanon\": \"lb\",\n  \"Lesotho\": \"ls\",\n  \"Liberia\": \"lr\",\n  \"Libyan Arab Jamahiriya\": \"ly\",\n  \"Liechtenstein\": \"li\",\n  \"Lithuania\": \"lt\",\n  \"Luxembourg\": \"lu\",\n  \"Macao\": \"mo\",\n  \"Madagascar\": \"mg\",\n  \"Malawi\": \"mw\",\n  \"Malaysia\": \"my\",\n  \"Maldives\": \"mv\",\n  \"Mali\": \"ml\",\n  \"Malta\": \"mt\",\n  \"Marshall Islands\": \"mh\",\n  \"Martinique\": \"mq\",\n  \"Mauritania\": \"mr\",\n  \"Mauritius\": \"mu\",\n  \"Mayotte\": \"yt\",\n  \"Mexico\": \"mx\",\n  \"Moldova\": \"md\",\n  \"Monaco\": \"mc\",\n  \"Mongolia\": \"mn\",\n  \"Montenegro\": \"me\",\n  \"Montserrat\": \"ms\",\n  \"Morocco\": \"ma\",\n  \"Mozambique\": \"mz\",\n  \"Myanmar\": \"mm\",\n  \"Namibia\": \"na\",\n  \"Nauru\": \"nr\",\n  \"Nepal\": \"np\",\n  \"Netherlands\": \"nl\",\n  \"Netherlands Antilles\": \"an\",\n  \"New Caledonia\": \"nc\",\n  \"New Zealand\": \"nz\",\n  \"Nicaragua\": \"ni\",\n  \"Niger\": \"ne\",\n  \"Nigeria\": \"ng\",\n  \"Niue\": \"nu\",\n  \"Norfolk Island\": \"nf\",\n  \"Northern Mariana Islands\": \"mp\",\n  \"Norway\": \"no\",\n  \"Oman\": \"om\",\n  \"Pakistan\": \"pk\",\n  \"Palau\": \"pw\",\n  \"Palestinian Territory (occupied)\": \"ps\",\n  \"Panama\": \"pa\",\n  \"Papua New Guinea\": \"pg\",\n  \"Paraguay\": \"py\",\n  \"Peru\": \"pe\",\n  \"Philippines\": \"ph\",\n  \"Pitcairn\": \"pn\",\n  \"Poland\": \"pl\",\n  \"Portugal\": \"pt\",\n  \"Portuguese Timor\": \"tp\",\n  \"Puerto Rico\": \"pr\",\n  \"Qatar\": \"qa\",\n  \"Republic of Korea (South Korea)\": \"kr\",\n  \"Reunion\": \"re\",\n  \"Romania\": \"ro\",\n  \"Russian Federation\": \"ru\",\n  \"Rwanda\": \"rw\",\n  \"Saint Barthelemy\": \"bl\",\n  \"Saint Helena\": \"sh\",\n  \"Saint Kitts and Nevis\": \"kn\",\n  \"Saint Lucia\": \"lc\",\n  \"Saint Martin\": \"mf\",\n  \"Saint Pierre and Miquelon\": \"pm\",\n  \"Saint Vincent and the Grenadines\": \"vc\",\n  \"Samoa\": \"ws\",\n  \"San Marino\": \"sm\",\n  \"Sao Tome and Principe\": \"st\",\n  \"Saudi Arabia\": \"sa\",\n  \"Senegal\": \"sn\",\n  \"Serbia\": \"rs\",\n  \"Seychelles\": \"sc\",\n  \"Sierra Leone\": \"sl\",\n  \"Singapore\": \"sg\",\n  \"Slovakia\": \"sk\",\n  \"Slovenia\": \"si\",\n  \"Solomon Islands\": \"sb\",\n  \"Somalia\": \"so\",\n  \"South Africa\": \"za\",\n  \"South Georgia and the South Sandwich Islands\": \"gs\",\n  \"Soviet Union\": \"su\",\n  \"Spain\": \"es\",\n  \"Sri Lanka\": \"lk\",\n  \"Sudan\": \"sd\",\n  \"Suriname\": \"sr\",\n  \"Svalbard and Jan Mayen\": \"sj\",\n  \"Swaziland\": \"sz\",\n  \"Sweden\": \"se\",\n  \"Switzerland\": \"ch\",\n  \"Syrian Arab Republic\": \"sy\",\n  \"Taiwan\": \"tw\",\n  \"Tajikistan\": \"tj\",\n  \"Thailand\": \"th\",\n  \"The Democratic Republic of the Congo\": \"cd\",\n  \"The Former Yugoslav Republic of Macedonia\": \"mk\",\n  \"Timor-Leste\": \"tl\",\n  \"Togo\": \"tg\",\n  \"Tokelau\": \"tk\",\n  \"Tonga\": \"to\",\n  \"Trinidad and Tobago\": \"tt\",\n  \"Tunisia\": \"tn\",\n  \"Turkey\": \"tr\",\n  \"Turkmenistan\": \"tm\",\n  \"Turks and Caicos Islands\": \"tc\",\n  \"Tuvalu\": \"tv\",\n  \"Uganda\": \"ug\",\n  \"Ukraine\": \"ua\",\n  \"United Arab Emirates\": \"ae\",\n  \"United Kingdom\": \"uk\",\n  \"United Kingdom\": \"gb\",\n  \"United Republic of Tanzania \": \"tz\",\n  \"United States\": \"us\",\n  \"United States Minor Outlying Islands\": \"um\",\n  \"Uruguay\": \"uy\",\n  \"Uzbekistan\": \"uz\",\n  \"Vanuatu\": \"vu\",\n  \"Venezuela\": \"ve\",\n  \"Viet Nam\": \"vn\",\n  \"Virgin Islands  British\": \"vg\",\n  \"Virgin Islands  US\": \"vi\",\n  \"Wallis and Futuna\": \"wf\",\n  \"Western Sahara\": \"eh\",\n  \"Yemen\": \"ye\",\n  \"Yugoslavia\": \"yu\",\n  \"Zambia\": \"zm\",\n  \"Zimbabwe\": \"zw\"\n};\n*/\n\nfunction getGeocodingRegion() {\n  return PropertiesService.getDocumentProperties().getProperty('GEOCODING_REGION') || 'us';\n}\n\n/*\nfunction setGeocodingRegion(region) {\n  PropertiesService.getDocumentProperties().setProperty('GEOCODING_REGION', region);\n  updateMenu();\n}\n\nfunction promptForGeocodingRegion() {\n  var ui = SpreadsheetApp.getUi();\n\n  var result = ui.prompt(\n    'Set the Geocoding Country Code (currently: ' + getGeocodingRegion() + ')',\n    'Enter the 2-letter country code (ccTLD) that you would like ' +\n    'the Google geocoder to search first for results. ' +\n    'For example: Use \\'uk\\' for the United Kingdom, \\'us\\' for the United States, etc. ' +\n    'For more country codes, see: https://en.wikipedia.org/wiki/Country_code_top-level_domain',\n    ui.ButtonSet.OK_CANCEL\n  );\n\n  // Process the user's response.\n  if (result.getSelectedButton() == ui.Button.OK) {\n    setGeocodingRegion(result.getResponseText());\n  }\n}\n*/\n\n// Forward Geocoding -- convert address to GPS position.\nfunction addressToPosition() {\n  var sheet = SpreadsheetApp.getActiveSheet();\n  var cells = sheet.getActiveRange();\n  \n  var popup = SpreadsheetApp.getUi();\n  \n  // Must have selected at least 3 columns (Address, Lat, Lng).\n  // Must have selected at least 1 row.\n  \n  var columnCount = cells.getNumColumns();\n  var rowCount = cells.getNumRows();\n\n  if (columnCount < 3) {\n    popup.alert(\"Select at least 3 columns: Address in the leftmost column(s); the geocoded Latitude, Longitude will go into the last 2 columns.\");\n    return;\n  }\n  \n  var addressRow;\n\n//  var addressColumnStart = 1; // Address data is in columns [1 .. columnCount - 2].\n//  var addressColumnStop  = columnCount - 2; \n  \n  var addressColumn;\n  \n  var latColumn = columnCount - 1; // Latitude  goes into the next-to-last column.\n  var lngColumn = columnCount;     // Longitude goes into the last column.\n  \n  var geocoder = Maps.newGeocoder().setRegion(getGeocodingRegion());\n  var location;\n\n  var addresses = sheet.getRange(cells.getRow(), cells.getColumn(), rowCount, columnCount - 2).getValues();\n  \n  // For each row of selected data...\n  for (addressRow = 1; addressRow <= rowCount; ++addressRow) {\n    var lat = cells.getCell(addressRow, latColumn).getValue();\n    var lng = cells.getCell(addressRow, lngColumn).getValue();\n\n    // Skip rows which are already processed\n    if (lat.length > 0 && lng.length > 0) continue;\n\n    var address = addresses[addressRow - 1].join(' ');\n\n    // Replace problem characters.\n    address = address.replace(/'/g, \"%27\");\n    address = address.trim();\n\n    // Skip blank addresses.\n    if (0 == address.length) continue;\n\n    Logger.log(address);\n    \n    // Geocode the address and plug the lat, lng pair into the \n    // last 2 elements of the current range row.\n    location = geocoder.geocode(address);\n   \n    Logger.log(location.status);\n\n    // Only change cells if geocoder seems to have gotten a \n    // valid response.\n    if (location.status == 'OK') {\n      lat = location[\"results\"][0][\"geometry\"][\"location\"][\"lat\"];\n      lng = location[\"results\"][0][\"geometry\"][\"location\"][\"lng\"];\n      \n      cells.getCell(addressRow, latColumn).setValue(lat);\n      cells.getCell(addressRow, lngColumn).setValue(lng);\n\n      Logger.log(lat);\n      Logger.log(lng);\n    } else {\n      Logger.log(location.status);\n    }\n  }\n};\n\n// Reverse Geocode -- GPS position to nearest address.\nfunction positionToAddress() {\n  var sheet = SpreadsheetApp.getActiveSheet();\n  var cells = sheet.getActiveRange();\n\n  var popup = SpreadsheetApp.getUi();\n  \n  // Must have selected at least 3 columns (Address, Lat, Lng).\n  // Must have selected at least 1 row.\n\n  var columnCount = cells.getNumColumns();\n\n  if (columnCount < 3) {\n    popup.alert(\"Select at least 3 columns: Latitude, Longitude in the first 2 columns; the reverse-geocoded Address will go into the last column.\");\n    return;\n  }\n\n  var latColumn     = 1;\n  var lngColumn     = 2;\n\n  var addressRow;\n  var addressColumn = columnCount;\n\n  var geocoder = Maps.newGeocoder().setRegion(getGeocodingRegion());\n  var location;\n  \n  for (addressRow = 1; addressRow <= cells.getNumRows(); ++addressRow) {\n    var lat = cells.getCell(addressRow, latColumn).getValue();\n    var lng = cells.getCell(addressRow, lngColumn).getValue();\n    \n    // Geocode the lat, lng pair to an address.\n    location = geocoder.reverseGeocode(lat, lng);\n   \n    // Only change cells if geocoder seems to have gotten a \n    // valid response.\n    Logger.log(location.status);\n    if (location.status == 'OK') {\n      var address = location[\"results\"][0][\"formatted_address\"];\n\n      cells.getCell(addressRow, addressColumn).setValue(address);\n    }\n  }\n};\n\n// Reverse Geocode -- GPS position to nearest address, broken out into components.\nfunction positionToAddressComponents() {\n  var sheet = SpreadsheetApp.getActiveSheet();\n  var cells = sheet.getActiveRange();\n  \n  // Must have selected 8 columns (Lat, Lng, +6 components).\n  // Must have selected at least 1 row.\n\n  var columnCount = cells.getNumColumns();\n\n  if (columnCount != 11) {\n    SpreadsheetApp.getUi().alert(\"Latitude, Longitude in the first 2 columns; the reverse-geocoded Address will go into the following columns.\");\n    return;\n  }\n\n  var latColumn     = 1;\n  var lngColumn     = 2;\n\n  var addressRow;\n  var addressColumn = 3;\n\n  var geocoder = Maps.newGeocoder().setRegion(getGeocodingRegion());\n  var location;\n  \n  for (addressRow = 1; addressRow <= cells.getNumRows(); ++addressRow) {\n    var lat = cells.getCell(addressRow, latColumn).getValue();\n    var lng = cells.getCell(addressRow, lngColumn).getValue();\n    \n    // Geocode the lat, lng pair to an address.\n    location = geocoder.reverseGeocode(lat, lng);\n   \n    // Only change cells if geocoder seems to have gotten a \n    // valid response.\n    //\n    // [{short_name=49, long_name=49, types=[street_number]}, {long_name=Bleibtreustraße, types=[route], short_name=Bleibtreustraße}, {long_name=Bezirk Charlottenburg-Wilmersdorf, types=[political, sublocality, sublocality_level_1], short_name=Bezirk Charlottenburg-Wilmersdorf}, {short_name=Berlin, types=[locality, political], long_name=Berlin}, {types=[administrative_area_level_1, political], short_name=Berlin, long_name=Berlin}, {short_name=DE, long_name=Germany, types=[country, political]}, {types=[postal_code], short_name=10623, long_name=10623}]\n\n    Logger.log(location.status);\n    if (location.status == 'OK') {\n      const L = location[\"results\"][0][\"address_components\"];\n      \n      const outStreetNumber  = getAddressComponent(L, 'street_number',               'short_name');\n      const outStreet        = getAddressComponent(L, 'route',                       'short_name');\n      const outBorough       = getAddressComponent(L, 'sublocality',                 'short_name');\n      const outCity          = getAddressComponent(L, 'locality',                    'short_name');\n      const outStateLong     = getAddressComponent(L, 'administrative_area_level_1', 'long_name');\n      const outStateShort    = getAddressComponent(L, 'administrative_area_level_1', 'short_name');\n      const outCountryLong   = getAddressComponent(L, 'country',                     'long_name');\n      const outCountryShort  = getAddressComponent(L, 'country',                     'short_name');\n      const outPostcodeShort = getAddressComponent(L, 'postal_code',                 'short_name');\n\n      cells.getCell(addressRow, addressColumn + 0).setValue(outStreetNumber);\n      cells.getCell(addressRow, addressColumn + 1).setValue(outStreet);\n      cells.getCell(addressRow, addressColumn + 2).setValue(outBorough);\n      cells.getCell(addressRow, addressColumn + 3).setValue(outCity);\n      cells.getCell(addressRow, addressColumn + 4).setValue(outStateLong);\n      cells.getCell(addressRow, addressColumn + 5).setValue(outStateShort);\n      cells.getCell(addressRow, addressColumn + 6).setValue(outCountryLong);\n      cells.getCell(addressRow, addressColumn + 7).setValue(outCountryShort);\n      cells.getCell(addressRow, addressColumn + 8).setValue(outPostcodeShort);\n    }\n  }\n};\n\nfunction getAddressComponent(result, whichType, whichName) {\n  for (let r of result) {\n    for (let t of r[\"types\"]) {\n      if (t === whichType) {\n        Logger.log(r[whichName]);\n        return r[whichName];\n      }\n    }\n  }\n  \n  return '';\n}\n\n// @param coords is an array of [lat, lng] arrays.\nfunction staticMapFromCoords(coords) {\n  let map = Maps.newStaticMap()\n    .setSize(1280, 720);\n\n  Logger.log(`Mapping ${coords.length} locations.`);\n\n  for (let c of coords) {\n    map.addMarker(c[0], c[1]);\n  }\n\n  return map;\n}\n\nfunction makePreview(map, sheet) {\n  // Add image to sheet. No wipeouts.\n\n  let allImages = sheet.getImages();\n  for (let i of allImages) {\n    i.remove();\n  }\n\n  const originCol = 4 + allImages.length;\n  const originRow = 3 + allImages.length;\n\n  let sheetImage = sheet.insertImage(map.getBlob(), originCol, originRow);\n  sheetImage.setAltTextDescription(map.getMapUrl);\n}\n\nfunction makeMap() {\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Mapping');\n\n  let range  = sheet.getRange('A3:B1000');\n  let values = range.getValues();\n\n  let filteredValues = values.filter((value) => { return (typeof(value[0]) === 'number' && typeof(value[1]) === 'number'); });\n\n  let map = staticMapFromCoords(filteredValues);\n\n  makePreview(map, sheet);\n}\n\nfunction generateMenu() {\n  // var setGeocodingRegionMenuItem = 'Set Geocoding Region (Currently: ' + getGeocodingRegion() + ')';\n  \n  // {\n  //   name: setGeocodingRegionMenuItem,\n  //   functionName: \"promptForGeocodingRegion\"\n  // },\n  \n  var entries = [{\n    name: \"Geocode Selected Cells (Address to Latitude, Longitude)\",\n    functionName: \"addressToPosition\"\n  },\n  {\n    name: \"Geocode Selected Cells (Latitude, Longitude to Address)\",\n    functionName: \"positionToAddress\"\n  },\n  {\n    name: \"Geocode Selected Cells (Latitude, Longitude to Address Components)\",\n    functionName: \"positionToAddressComponents\"\n  },\n  {\n    name: \"Map Cells In Mapping Sheet (Latitude, Longitude -> Map Image)\",\n    functionName: \"makeMap\"\n  }\n  ];\n  \n  return entries;\n}\n\nfunction updateMenu() {\n  SpreadsheetApp.getActiveSpreadsheet().updateMenu('Geocode', generateMenu())\n}\n\n/**\n * Adds a custom menu to the active spreadsheet, containing a single menu item\n * for invoking the readRows() function specified above.\n *\n * The onOpen() function, when defined, is automatically invoked whenever the\n * spreadsheet is opened.\n *\n * For more information on using the Spreadsheet API, see\n * https://developers.google.com/apps-script/service_spreadsheet\n */\nfunction onOpen() {\n  if (mapsClientId && mapsSigningKey) Maps.setAuthentication(mapsClientId, mapsSigningKey);\n\n  SpreadsheetApp.getActiveSpreadsheet().addMenu('Geocode', generateMenu());\n};\n"
  },
  {
    "path": "README.md",
    "content": "# Google Sheets Geocoding Macro\n\n![How It Works](images/google-sheets-geocoding-macro.gif)\n\nGeocode from addresses to latitude / longitude, and vice versa using Google\nSheets.\n\n## ~~Test Sheet~~\n\n> ~~Try the script out on a [Test\nSheet](https://docs.google.com/spreadsheets/d/1tkzPt_yGfFG2MOs6-xBodajY79_WV8s4LpU6mhszAk4/edit?usp=sharing)\nwith sample address data. You can enter your own address data and geocode it  in\nthe rows below.~~\n>\n> ~~You **must** be logged into a Google Account before the Geocode menu will\nappear.~~\n>\n> ~~Any data you enter will be automatically deleted every Sunday at 4AM CEST, this\nisn't for long term storage.~~\n\n## Nope.\n\nTest Sheet is removed due to:\n\n* Google not locking down Apps Script **editing** capabilities for **Viewers** on the Sheet.\n  Because you can lock *cells*, but you can't lock *code*! Minor oversight!\n* too many people then editing the Apps Script code\n* too many people then breaking the Apps Script code and not fixing it afterwards\n* people immediately breaking the Cleanup code so that their data stays forever\n  (let's boil the oceans together!)\n* people adding random Extensions to this shared Sheet\n* Google not providing a way *at all* to remove Extensions that Viewers have added\n  ![](images/duh-no-way-to-remove-1.png)\n* people associating the Sheet with their own Google Cloud Platform projects\n  and breaking it for me and everyone else\n  ![](images/duh-no-way-to-remove-4.png)\n* Google not providing any sensible or quick ways to disassociate other peoples' GCP Project IDs\n  (no I do not want to screw around in the Google Cloud Console all afternoon)\n  ![](images/duh-no-way-to-remove-2.png)\n* me getting notifications from Google every time people now want access to the sheet\n  I created publicly, when access was not an issue before (to be fair, this probably\n  changed as Google changed things in their backend to compel authentication to a\n  once-public API)\n* ... other idiotic shenanigans\n\n**Good luck, this was a great experiment in the tragedy of the commons.**\n\nA few days ago, someone dropped a yogurt in the entrance hall to my apartment building\nand didn't clean it up. It was still there > 24 hours later, curdling into sour cream. Cool.\n\n![](images/duh-tragedy-of-the-commons-1.jpg)\n\n## Multicolumn Addresses &rarr; Latitude, Longitude\n\nNow it supports geocoding using address data spread across multiple columns.\n\nThe way this works is: You select a set of columns containing the data, and the\ngeocoding process puts the latitude, longitude data in the **rightmost two\ncolumns**. It will overwrite any data in those two columns.\n\nSome care is needed, as it will concatenate all columns except the rightmost two\ncolumns to create the address string.\n\n![Multicolumn Address Geocoding](images/google-sheets-geocoding-macro-forward.png)\n\n## Latitude, Longitude &rarr; Nearest Address\n\nIt also supports reverse geocoding.\n\nSimply select the latitude, longitude columns and it will place the nearest\naddress data in the rightmost column. It will overwrite any data in that column.\n\nLess care is needed, as it will automatically use the **leftmost two columns** as\nthe latitude, longitude pair.\n\n![Reverse Geocoding](images/google-sheets-geocoding-macro-reverse.png)\n\n## Latitude, Longitude &rarr; Address Components\n\nIt now supports reverse geocoding and splitting the address components into\ndifferent columns.\n\nSee the Reverse To Components tab in the [Test\nSheet](https://docs.google.com/spreadsheets/d/1tkzPt_yGfFG2MOs6-xBodajY79_WV8s4LpU6mhszAk4/edit?usp=sharing).\n\n![Reverse Geocoding to Address\nComponents](images/google-sheets-geocoding-macro-reverse-to-components.apng)\n\n## Map Cells\n\nIt now supports mapping the Latitude, Longitude pairs in the Mapping tab.\n\n![Mapping Coordinates](images/google-sheets-geocoding-macro-mapping-points.apng)\n\n## Adding It To Your Own Sheet\n\nStep 1. Create or Open a Google Sheet and add addresses to it.\n\n![open google sheet](images/step-01-open-sheet.png)\n\nStep 2. Tools -> Script Editor\n\n![edit the script](images/step-02-script-editor.png)\n\nStep 3. Copy [this script\ncode](https://raw.githubusercontent.com/nuket/google-sheets-geocoding-macro/master/Code.gs)\ninto the Code.gs editor, replacing everything.\n\n![use geocoding script code](images/step-03-script-editor.png)\n\nStep 4. Save\n\n![save code](images/step-04-script-editor.png)\n\nStep 5. Reload Sheet\n\n![reload sheet](images/step-05-geocode-menu-appears.apng)\n\nStep 6. Run Geocode, Click Through Warnings\n\n![run geocode, click through warnings](images/step-06-geocode-and-warnings.apng)\n\nThat's it.\n\n## Troubleshooting\n\n* I don't see the Geocode menu!\n\n  You **must** be logged into a Google Account before the Geocode menu will\n  appear. Anonymous / not logged-in users will not work, Incognito Mode will not\n  work.\n\n* It gives me a bunch of warnings when I run it the first time.\n\n  If you're using the Test Sheet, this means that the script will have access to\n  the data you are entering. Don't enter anything you wouldn't want me to see,\n  because as the owner of the shared Sheet, I see the data that gets put into\n  it.\n\n  If you've added the script to your own sheet, this means that the script will\n  have access to the data you are entering. Since you're the owner of your\n  Sheet, this isn't an issue. You can always audit the script by reading the\n  source code in this repository.\n\n* It returns latitude / longitude data using \",\" instead of \".\" separators.\n\n  There's not much I can do about the return formats, but a user reported that\n  adding the following array formula to the latitude / longitude columns changes\n  the separators for them: `=ARRAYFORMULA(SUBSTITUTE(C2:C;\",\";\".\"))`.\n\n  Make sure you specify the correct columns.\n"
  }
]