Source: GeoHash.js

// geohash.js
// Geohash library for Javascript
// (c) 2008 David Troy
// Distributed under the MIT License
// Original at: https://github.com/davetroy/geohash-js

// Modified by Irrelon Software Limited (http://www.irrelon.com)
// to clean up and modularise the code using Node.js-style exports
// and add a few helper methods.
// @by Rob Evans - rob@irrelon.com
"use strict";

/*
Define some shared constants that will be used by all instances
of the module.
 */
var bits,
	base32,
	neighbors,
	borders,
	PI180 = Math.PI / 180,
	PI180R = 180 / Math.PI,
	earthRadius = 6371; // mean radius of the earth

bits = [16, 8, 4, 2, 1];

base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
neighbors = {
	right: {even: "bc01fg45238967deuvhjyznpkmstqrwx"},
	left: {even: "238967debc01fg45kmstqrwxuvhjyznp"},
	top: {even: "p0r21436x8zb9dcf5h7kjnmqesgutwvy"},
	bottom: {even: "14365h7k9dcfesgujnmqp0r2twvyx8zb"}
};

borders = {
	right: {even: "bcfguvyz"},
	left: {even: "0145hjnp"},
	top: {even: "prxz"},
	bottom: {even: "028b"}
};

neighbors.bottom.odd = neighbors.left.even;
neighbors.top.odd = neighbors.right.even;
neighbors.left.odd = neighbors.bottom.even;
neighbors.right.odd = neighbors.top.even;

borders.bottom.odd = borders.left.even;
borders.top.odd = borders.right.even;
borders.left.odd = borders.bottom.even;
borders.right.odd = borders.top.even;

var GeoHash = function () {};

/**
 * Converts degrees to radians.
 * @param {Number} degrees
 * @return {Number} radians
 */
GeoHash.prototype.radians = function radians (degrees) {
	return degrees * PI180;
};

/**
 * Converts radians to degrees.
 * @param {Number} radians
 * @return {Number} degrees
 */
GeoHash.prototype.degrees = function (radians) {
	return radians * PI180R;
};

GeoHash.prototype.refineInterval = function (interval, cd, mask) {
	if (cd & mask) { //jshint ignore: line
		interval[0] = (interval[0] + interval[1]) / 2;
	} else {
		interval[1] = (interval[0] + interval[1]) / 2;
	}
};

/**
 * Calculates all surrounding neighbours of a hash and returns them.
 * @param {String} centerHash The hash at the center of the grid.
 * @param options
 * @returns {*}
 */
GeoHash.prototype.calculateNeighbours = function (centerHash, options) {
	var response;

	if (!options || options.type === 'object') {
		response = {
			center: centerHash,
			left: this.calculateAdjacent(centerHash, 'left'),
			right: this.calculateAdjacent(centerHash, 'right'),
			top: this.calculateAdjacent(centerHash, 'top'),
			bottom: this.calculateAdjacent(centerHash, 'bottom')
		};

		response.topLeft = this.calculateAdjacent(response.left, 'top');
		response.topRight = this.calculateAdjacent(response.right, 'top');
		response.bottomLeft = this.calculateAdjacent(response.left, 'bottom');
		response.bottomRight = this.calculateAdjacent(response.right, 'bottom');
	} else {
		response = [];

		response[4] = centerHash;
		response[3] = this.calculateAdjacent(centerHash, 'left');
		response[5] = this.calculateAdjacent(centerHash, 'right');
		response[1] = this.calculateAdjacent(centerHash, 'top');
		response[7] = this.calculateAdjacent(centerHash, 'bottom');

		response[0] = this.calculateAdjacent(response[3], 'top');
		response[2] = this.calculateAdjacent(response[5], 'top');
		response[6] = this.calculateAdjacent(response[3], 'bottom');
		response[8] = this.calculateAdjacent(response[5], 'bottom');
	}

	return response;
};

/**
 * Calculates a new lat/lng by travelling from the center point in the
 * bearing specified for the distance specified.
 * @param {Array} centerPoint An array with latitude at index 0 and
 * longitude at index 1.
 * @param {Number} distanceKm The distance to travel in kilometers.
 * @param {Number} bearing The bearing to travel in degrees (zero is
 * north).
 * @returns {{lat: Number, lng: Number}}
 */
GeoHash.prototype.calculateLatLngByDistanceBearing = function (centerPoint, distanceKm, bearing) {
	var curLon = centerPoint[1],
		curLat = centerPoint[0],

		destLat = Math.asin(Math.sin(this.radians(curLat)) * Math.cos(distanceKm / earthRadius) + Math.cos(this.radians(curLat)) * Math.sin(distanceKm / earthRadius) * Math.cos(this.radians(bearing))),
 		tmpLon = this.radians(curLon) + Math.atan2(Math.sin(this.radians(bearing)) * Math.sin(distanceKm / earthRadius) * Math.cos(this.radians(curLat)), Math.cos(distanceKm / earthRadius) - Math.sin(this.radians(curLat)) * Math.sin(destLat)),
		destLon = (tmpLon + 3 * Math.PI) % (2 * Math.PI) - Math.PI;  // normalise to -180..+180ยบ

	return {
		lat: this.degrees(destLat),
		lng: this.degrees(destLon)
	};
};

/**
 * Calculates the extents of a bounding box around the center point which
 * encompasses the radius in kilometers passed.
 * @param {Array} centerPoint An array with latitude at index 0 and
 * longitude at index 1.
 * @param radiusKm Radius in kilometers.
 * @returns {{lat: Array, lng: Array}}
 */
GeoHash.prototype.calculateExtentByRadius = function (centerPoint, radiusKm) {
	var maxWest,
		maxEast,
		maxNorth,
		maxSouth,
		lat = [],
		lng = [];

	maxNorth = this.calculateLatLngByDistanceBearing(centerPoint, radiusKm, 0);
	maxEast = this.calculateLatLngByDistanceBearing(centerPoint, radiusKm, 90);
	maxSouth = this.calculateLatLngByDistanceBearing(centerPoint, radiusKm, 180);
	maxWest = this.calculateLatLngByDistanceBearing(centerPoint, radiusKm, 270);

	lat[0] = maxNorth.lat;
	lat[1] = maxSouth.lat;

	lng[0] = maxWest.lng;
	lng[1] = maxEast.lng;

	return {
		lat: lat,
		lng: lng
	};
};

/**
 * Calculates all the geohashes that make up the bounding box that surrounds
 * the circle created from the center point and radius passed.
 * @param {Array} centerPoint An array with latitude at index 0 and
 * longitude at index 1.
 * @param {Number} radiusKm The radius in kilometers to encompass.
 * @param {Number} precision The number of characters to limit the returned
 * geohash strings to.
 * @returns {Array} The array of geohashes that encompass the bounding box.
 */
GeoHash.prototype.calculateHashArrayByRadius = function (centerPoint, radiusKm, precision) {
	var extent = this.calculateExtentByRadius(centerPoint, radiusKm),
		northWest = [extent.lat[0], extent.lng[0]],
		northEast = [extent.lat[0], extent.lng[1]],
		southWest = [extent.lat[1], extent.lng[0]],
		northWestHash = this.encode(northWest[0], northWest[1], precision),
		northEastHash = this.encode(northEast[0], northEast[1], precision),
		southWestHash = this.encode(southWest[0], southWest[1], precision),
		hash,
		widthCount = 0,
		heightCount = 0,
		widthIndex,
		heightIndex,
		hashArray = [];

	hash = northWestHash;
	hashArray.push(hash);

	// Walk from north west to north east until we find the north east geohash
	while (hash !== northEastHash) {
		hash = this.calculateAdjacent(hash, 'right');
		widthCount++;

		hashArray.push(hash);
	}

	hash = northWestHash;

	// Walk from north west to south west until we find the south west geohash
	while (hash !== southWestHash) {
		hash = this.calculateAdjacent(hash, 'bottom');
		heightCount++;
	}

	// We now know the width and height in hash boxes of the area, fill in the
	// rest of the hashes into the hashArray array
	for (widthIndex = 0; widthIndex <= widthCount; widthIndex++) {
		hash = hashArray[widthIndex];

		for (heightIndex = 0; heightIndex < heightCount; heightIndex++) {
			hash = this.calculateAdjacent(hash, 'bottom');
			hashArray.push(hash);
		}
	}

	return hashArray;
};

/**
 * Calculates an adjacent hash to the hash passed, in the direction
 * specified.
 * @param {String} srcHash The hash to calculate adjacent to.
 * @param {String} dir Either "top", "left", "bottom" or "right".
 * @returns {String} The resulting geohash.
 */
GeoHash.prototype.calculateAdjacent = function (srcHash, dir) {
	srcHash = srcHash.toLowerCase();

	var lastChr = srcHash.charAt(srcHash.length - 1),
		type = (srcHash.length % 2) ? 'odd' : 'even',
		base = srcHash.substring(0, srcHash.length - 1);

	if (borders[dir][type].indexOf(lastChr) !== -1) {
		base = this.calculateAdjacent(base, dir);
	}

	return base + base32[neighbors[dir][type].indexOf(lastChr)];
};

/**
 * Decodes a string geohash back to a longitude/latitude array.
 * The array contains three latitudes and three longitudes. The
 * first of each is the lower extent of the geohash bounding box,
 * the second is the upper extent and the third is the center
 * of the geohash bounding box.
 * @param {String} geohash The hash to decode.
 * @returns {Object}
 */
GeoHash.prototype.decode = function (geohash) {
	var isEven = 1,
		lat = [],
		lon = [],
		i, c, cd, j, mask,
		latErr,
		lonErr;

	lat[0] = -90.0;
	lat[1] = 90.0;
	lon[0] = -180.0;
	lon[1] = 180.0;

	latErr = 90.0;
	lonErr = 180.0;

	for (i = 0; i < geohash.length; i++) {
		c = geohash[i];
		cd = base32.indexOf(c);

		for (j = 0; j < 5; j++) {
			mask = bits[j];

			if (isEven) {
				lonErr /= 2;
				this.refineInterval(lon, cd, mask);
			} else {
				latErr /= 2;
				this.refineInterval(lat, cd, mask);
			}

			isEven = !isEven;
		}
	}

	lat[2] = (lat[0] + lat[1]) / 2;
	lon[2] = (lon[0] + lon[1]) / 2;

	return {
		lat: lat,
		lng: lon
	};
};

/**
 * Encodes a longitude/latitude to geohash string.
 * @param latitude
 * @param longitude
 * @param {Number=} precision Length of the geohash string. Defaults to 12.
 * @returns {String}
 */
GeoHash.prototype.encode = function (latitude, longitude, precision) {
	var isEven = 1,
		mid,
		lat = [],
		lon = [],
		bit = 0,
		ch = 0,
		geoHash = "";

	if (!precision) { precision = 12; }

	lat[0] = -90.0;
	lat[1] = 90.0;
	lon[0] = -180.0;
	lon[1] = 180.0;

	while (geoHash.length < precision) {
		if (isEven) {
			mid = (lon[0] + lon[1]) / 2;

			if (longitude > mid) {
				ch |= bits[bit]; //jshint ignore: line
				lon[0] = mid;
			} else {
				lon[1] = mid;
			}
		} else {
			mid = (lat[0] + lat[1]) / 2;

			if (latitude > mid) {
				ch |= bits[bit]; //jshint ignore: line
				lat[0] = mid;
			} else {
				lat[1] = mid;
			}
		}

		isEven = !isEven;

		if (bit < 4) {
			bit++;
		} else {
			geoHash += base32[ch];
			bit = 0;
			ch = 0;
		}
	}

	return geoHash;
};

if (typeof module !== 'undefined') { module.exports = GeoHash; }