Geocoding is the process of taking latitude and longitude to determine an address, or taking an address and producing latitude and longitude coordinates.

There are numerous reasons an application needs to use a geocoder. For Now Serving, we use it during the sign up process, as well as making it easy to find nearby restaurants with a single click of ‘Find Me’.

Let’s get to coding!

Rails API

We’ll need to grab the mapbox-sdk and add it to the Gemfile.

gem 'mapbox-sdk', '~>2'

Create a simple initializer to set the access token in your app (e.g config/initializers/mapbox.rb )

Mapbox.access_token = MAPBOX_ACCESS_TOKEN

Next, let’s add a couple routes:

namespace :address_search do

get 'expand', to: 'expand'

get 'parse', to: 'parse'

end

And an address_search_controller.rb :

class AddressSearchController < ApplicationController



# Take an addresss and return lat/lng

def expand

begin

@addresses = Mapbox::Geocoder.geocode_forward(address_params[:a]) unless address_params[:a].nil?

render template: 'address_search/result'

rescue StandardError

render json: { errors: ['Unable to perform forward geocoding'] }

end

end



# Take lat/lng array and return a postal address

def parse

begin

@location = { latitude: address_params[:latitude].to_f, longitude: address_params[:longitude].to_f }

@addresses = Mapbox::Geocoder.geocode_reverse(@location)

render template: 'address_search/result'

rescue StandardError

render json: { errors: ['Unable to perform reverse geocoding'] }

end

end



private



def address_params

params.permit(:a, :latitude, :longitude)

end

end

The expand method takes the a query params and asks the geocoder service to return a latitude/longitude array. For getting an address from lat/lng we are expecting a hash like { latitude: 0, longitude: 0 } .

You may not want to render a template here but in my case I wanted to always return an array, so the best way to ensure that happened was rendering it with jbuilder one-liner:

json.array! @addresses

And a request spec:

RSpec.describe 'Address Search' do



it 'parses an address and returns latitude and longitude' do

get '/api/v1/address_search/expand', params: { a: '401 B St, San Diego CA' }

expect(response).to be_successful

end



it 'parses latitude and longitude and returns an address' do

get '/api/v1/address_search/parse', params: { longitude: 127.0, latitude: -43.64}

expect(response).to be_successful

end

end

Front End

We’re using the awesome NuxtJS framework for our UI. If you haven’t used it before, definitely give it a look. If you can’t use it, don’t worry; this code will work fine without Nuxt.

We use Vuex actions to call our back end, so we have a store for our Mapbox configuration.

export const actions = {

locate({ commit }, { longitude, latitude }) {

return this.$axios.get('/address_search/parse', { params: { longitude: longitude, latitude: latitude } })

},

coordinate({ commit }, params) {

return this.$axios.get('/address_search/expand', { params: { a: params } })

}

}

For presentation, we use vue-i18n, vue-notify, bootstrap-vue and vue-fontawesome .

<template>

<b-btn

v-b-tooltip.hover="true"

:data-state="state"

:variant="btnVariant"

:title="locationLabel"

type="button"

@click="findMe">

<font-awesome-icon v-if="state === 1" :icon="['far', 'spinner']" spin />

<font-awesome-icon v-else :icon="['far', 'location-arrow']" />

</b-btn>

</template>

<script>

import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'



export default {

components: {

FontAwesomeIcon

},

props: {

locationLabel: {

default: 'Find my current location',

type: String

}

},

data() {

return {

state: 0

}

},

computed: {

btnVariant() {

switch (this.state) {

case 0:

return 'outline-primary'

case 1:

return 'info'

case 2:

return 'success'

default:

return 'outline-primary'

}

}

},

methods: {

findMe() {

const vm = this

this.state = 1

if (!navigator.geolocation) {

vm.$notify({ text: vm.$t('geolocation.not_supported'), group: 'alerts' })

return

}



function success(position) {

const accuracy = position.coords.accuracy

vm.$store.dispatch('mapbox/locate', {

latitude: position.coords.latitude,

longitude: position.coords.longitude,

accuracy: accuracy

})

.then((resp) => {

vm.state = 2

vm.$emit('result', { name: resp.data[0].features[0].place_name, center: resp.data[0].features[0].center })

})

.catch(() => {

vm.state = 0

vm.$notify({ text: vm.$t('geolocation.not_found'), type: 'warning', group: 'alerts' })

})

}



function error() {

vm.$notify({ text: vm.$t('geolocation.not_found'), group: 'alerts', type: 'warning' })

}



navigator.geolocation.getCurrentPosition(success, error)

}

}

}

</script>

There’s a lot going on here, so lets break it all down.

The location button has three states; default state, active state, and a success state. The computed property handles changing out the css classes for each state.

There is also a tooltip that displays on hover to explain that the browser will ask for permission to send location information to the back end.

The findMe method is called on click. In it we have two callbacks for success and error that the browser’s built in getCurrentPosition needs to work correctly. When the browser provides the latitude and longitude to the success callback, we can send that to the back end using the Vuex action. Once the back end response comes, the component emits a result event containing the address name and coordinates. If permission is denied, we display an error notification. Also if the browser doesn’t support location services, we notify the user of that case.

Conclusion

Congrats you have a fully implemented API for forward and reverse geocoding solution!