Búsqueda parcial de direcciones / escritura anticipada

We have an address field we want to provide typeahead for. It sits behind a login, although if we needed to we could get crafty and make that one page public for licensing compliance.

The Google Maps API is getting locked down. We used to use the "reverse geocode" portion of it to perform partial address search / typeahead for addresses - so for example if the user typed:

1600 penn

I could hit the service and get back several suggestions, like:

1600 Pennsylvania Avenue, Washington, DC

There are several other partial address searches out there I've come across but they each have problems.

Google: $10000/yr minimum for just 7500 requests/day - ridiculous

Yahoo: Shutdown this year

Bing: Requires the page to be public / not behind login. This isn't a hard stop for us, but it would be a challenging redesign of how the page works.

Mapquest OpenStreetMap API: Searches for the exact string rather than a leading string - so it returns Penn Station instead of 1600 Pennsylvania Ave.

Mapquest OpenStreetMap data: We could download all of this and implement our own, but the CPU and data requirements would likely be too much to bite off for the time being.

We're fine with paying for usage, we'd just seek a solution closer to Amazon's $0.01/10000 requests than Google's.

preguntado el 08 de noviembre de 11 a las 09:11

3 Respuestas

ACTUALIZACIÓN: My original answer, intact below, was given before SmartyStreets had an address autocomplete offering, which is free with a LiveAddress API subscription.


It's simple, really. The USPS has approved certain vendors for address verification/standardization services. I work for one such company, SmartyStreets, and in my experience, what you are trying to do is probably easier than you think with a good API that has a REST endpoint. Look into the LiveAddress API.

Submit a street address and at least a city/state, or zip, or both, and you'll get a list of suggested results.

NOTICE, however, that non-CASS providers such as Google Maps do address approximation, not address validation. Google - and others - make a "best guess" which is what Google and them are expert at. If you want actual existing addresses, make sure you find a service that does just that. I'll add that LiveAddress is free now and offers better performance than, for example, the USPS API.

Respondido 27 Jul 20, 13:07

This seems ok if you're requiring the user to type a full address before validation, but the cost could get out of control if you're implementing a true autocomplete (like the one on the company's front page, for example). The usage limit on the free tier makes it useless for anything other than demoing the product. - ay

@ach Actually, SmartyStreets' autocomplete service is totally free with an account, and does type-ahead like on the homepage that you saw there: smartystreets.com/kb/faq/autocomplete-endpoint - Matt

How comes that doing it yourself isn't an option? IMO a partial search or a typeahead isn't so hard to do with a ternary trie on the address, street, city etc.pp field.

respondido 08 nov., 11:14

The client's server is already under a high load. A larger offering (like Google) has edge-cached responses to these queries where our best case would be considerably slower. And it seems like one of those big shared problems that ought to have an answer. - Chris Moschini

Google ha lanzado Google Places Autocomplete which resolves exactly this problem.

At the bottom of a page throw this in there:

<script defer async src="//maps.googleapis.com/maps/api/js?libraries=places&key=(key)&sensor=false&callback=googPlacesInit"></script>

Dónde (key) is your API key.

We've set our code up so you mark some fields to handle typeahead and get filled in by that typeahead, like:

<input name=Address placeholder=Address />
<input name=Zip placeholder=Zip />

etc.

Then you initialize it (before the Google Places API has loaded typically, since that's going to land async) with:

GoogleAddress.init('#billing input', '#shipping input');

Or whatever. In this case it's tying the address typeahead to whatever input has name=Address in the #billing tag and #shipping tag, and it will fill in the related fields inside those tags for City, State, Zip etc when an address is chosen.

Setup the class:

var GoogleAddress = {
    AddressFields: [],
    //ZipFields: [],    // Not in use and the support code is commented out, for now
    OnSelect: [],

    /**
     * @param {String} field Pass as many arguments as you like, each a selector to a set of inputs that should use Google
     * Address Typeahead via the Google Places API.
     * 
     * Mark the inputs with name=Address, name=City, name=State, name=Zip, name=Country
     * All fields are optional; you can for example leave Country out and everything else will still work.
     * 
     * The Address field will be used as the typeahead field. When an address is picked, the 5 fields will be filled in.
     */
    init: function (field) {
        var args = $.makeArray(arguments);
        GoogleAddress.AddressFields = $.map(args, function (selector) {
            return $(selector);
        });
    }
};

The script snippet above is going to async call into a function named googPlacesInit, so everything else is wrapped in a function by that name:

function googPlacesInit() {
    var fields = GoogleAddress.AddressFields;

    if (
        // If Google Places fails to load, we need to skip running these or the whole script file will fail
        typeof (google) == 'undefined' ||
        // If there's no input there's no typeahead so don't bother initializing
        fields.length == 0 || fields[0].length == 0
    )
        return;

Setup the autocomplete event, and deal with the fact that we always use multiple address fields, but Google wants to dump the entire address into a single input. There's surely a way to prevent this properly, but I'm yet to find it.

$.each(fields, function (i, inputs) {
    var jqInput = inputs.filter('[name=Address]');

    var addressLookup = new google.maps.places.Autocomplete(jqInput[0], {
        types: ['address']
    });
    google.maps.event.addListener(addressLookup, 'place_changed', function () {
        var place = addressLookup.getPlace();

        // Sometimes getPlace() freaks out and fails - if so do nothing but blank out everything after comma here.
        if (!place || !place.address_components) {
            setTimeout(function () {
                jqInput.val(/^([^,]+),/.exec(jqInput.val())[1]);
            }, 1);
            return;
        }

        var a = parsePlacesResult(place);

        // HACK! Not sure how to tell Google Places not to set the typeahead field's value, so, we just wait it out
        // then overwrite it
        setTimeout(function () {
            jqInput.val(a.address);
        }, 1);

        // For the rest, assign by lookup
        inputs.each(function (i, input) {
            var val = getAddressPart(input, a);
            if (val)
                input.value = val;
        });

        onGoogPlacesSelected();
    });

    // Deal with Places API blur replacing value we set with theirs
    var removeGoogBlur = function () {
        var googBlur = jqInput.data('googBlur');
        if (googBlur) {
            jqInput.off('blur', googBlur).removeData('googBlur');
        }
    };

    removeGoogBlur();
    var googBlur = jqInput.blur(function () {
        removeGoogBlur();
        var val = this.value;
        var _this = this;
        setTimeout(function () {
            _this.value = val;
        }, 1);
    });
    jqInput.data('googBlur', googBlur);
});

// Global goog address selected event handling
function onGoogPlacesSelected() {
    $.each(GoogleAddress.OnSelect, function (i, fn) {
        fn();
    });
}

Parsing a result into the canonical street1, street2, city, state/province, zip/postal code is not trivial. Google differentiates these localities with varying tags depending on where in the world you are, and as a warning, if you're used to US addresses, there are places for example in Africa that meet none of your expectations of what an address looks like. You can break addresses in the world into 3 categories:

  • US-identical - the entire US and several countries that use a similar addressing system

  • Formal addresses - UK, Australia, China, basically developed countries - but the way their address parts are broken up/locally written has a fair amount of variance

  • No formal addresses - in undeveloped areas there are no street names let alone street numbers, sometimes not even a town/city name, and certainly no zip. In these locations what you really want is a GPS location, which is not handled by this code.

This code only attempts to deal with the first 2 cases.

function parsePlacesResult(place) {
    var a = place.address_components;

    var p = {};
    var d = {};

    for (var i = 0; i < a.length; i++) {
        var ai = a[i];
        switch (ai.types[0]) {
            case 'street_number':
                p.num = ai.long_name;
                break;
            case 'route':
                p.rd = ai.long_name;
                break;
            case 'locality':
            case 'sublocality_level_1':
            case 'sublocality':
                d.city = ai.long_name;
                break;
            case 'administrative_area_level_1':
                d.state = ai.short_name;
                break;
            case 'country':
                d.country = ai.short_name;
                break;
            case 'postal_code':
                d.zip = ai.long_name;
        }
    }

    var addr = [];
    if (p.num)
        addr.push(p.num);
    if (p.rd)
        addr.push(p.rd);

    d.address = addr.join(' ');

    return d;
}

/**
 * @param input  An Input tag, the DOM element not a jQuery object
 * @paran a     A Google Places Address object, with props like .city, .state, .country...
 */
var getAddressPart = function(input, a) {
    switch(input.name) {
        case 'City': return a.city;
        case 'State': return a.state;
        case 'Zip': return a.zip;
        case 'Country': return a.country;
    }
    return null;
}

Respuesta antigua

ArcGis/ESRI has a limited typeahead solution that's functional but returns limited results only after quite a bit of input. There's a demo here:

http://www.esri.com/services/disaster-response/wildlandfire/latest-news-map.html

For example you might type 1600 Pennsylvania Ave hoping to get the white house by the time you type "1600 Penn", but have to get as far as "1600 pennsylvania ave, washington dc" before it responds with that address. Still, it could have a small benefit to users in time savings.

Respondido el 04 de junio de 20 a las 17:06

This can be a solution, the issue is that Google doesn't give real addresses. Instead, they approximate addresses based on where they should* be if they existed. They also aren't going to provide typeahead suggestions for apartment numbers. That is great if you are doing this for mapping & navigation purposes but not great if it is going to be used for mailing/shipping addresses. - Davin

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.