iD.Hash = function() {
    var hash = {},
        s0, // cached location.hash
        lat = 90 - 1e-8, // allowable latitude range
        map;

    var parser = function(map, s) {
        var q = iD.util.stringQs(s);
        var args = (q.map || '').split("/").map(Number);
        if (args.length < 3 || args.some(isNaN)) {
            return true; // replace bogus hash
        } else {
            map.zoom(args[0])
                .center([args[2], Math.min(lat, Math.max(-lat, args[1]))]);
        }
    };

    var formatter = function(map) {
        var center = map.center(),
            zoom = map.zoom(),
            precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
        return '#?map=' + zoom.toFixed(2) +
            '/' + center[1].toFixed(precision) +
            '/' + center[0].toFixed(precision);
    };

    var move = _.throttle(function() {
        var s1 = formatter(map);
        if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
    }, 1000);

    function hashchange() {
        if (location.hash === s0) return; // ignore spurious hashchange events
        if (parser(map, (s0 = location.hash).substring(2)))
            move(); // replace bogus hash
    }

    hash.map = function(x) {
        if (!arguments.length) return map;
        if (map) {
            map.off("move", move);
            window.removeEventListener("hashchange", hashchange, false);
        }
        map = x;
        if (x) {
            map.on("move", move);
            window.addEventListener("hashchange", hashchange, false);
            if (location.hash) {
                hashchange();
                hash.hadHash = true;
            }
        }
        return hash;
    };

    return hash;
};
