One of our systems at work allowed users to fill in an address only using the zip code. It would do a reverse lookup against a database and bring back the city and the state. Unfortunately when upgrading the system, this part broke. I wrote some one off code to replace this functionality using an ajax call to the Google Maps API.

The call is based off of the google maps api. The theory is you pass a zip code in the address and parse the returned json object.

For examples sake we are going to have a basic form.
<form>
    <label for=full_name>Full Name</label>
    <input type=text id="full_name" name="full_name">
    <label for=address1>Address1</label>
    <input type=text id="address1" name="address1">
    <label for=address2>Address2</label>
    <input type=text id="address2" name="address2">
    <label for=zipcode>Zip Code</label>
    <input type=text id="zipcode" name="zipcode" onblur="getCityState()">
    <label for=city>City</label>
    <input type=text id="city" name="city">
    <label for=state>State</label>
    <input type=text id="state" name="state">
</form>
Now for a little jquery to validate.
function getCityState(){
    var url = "http://maps.googleapis.com/maps/api/geocode/json?address="+ $('#zipcode').val() +"&sensor=true"; //google maps api location
    $.ajax({
        url:url,
        crossDomain: true,
        type:'get',
        dataType:'json',
        error:function(xhr, status, errorThrown) {
            console.log(errorThrown+'\n'+status+'\n'+xhr.statusText);
        },
        success:function(data){
            var address = data['results'][0]['formatted_address'].split(',');
            var state = address[1].split(' ');
            $('#city').val(address[0]);
            $('#state').val(state[1]);
        }
    });
}
On a side note the google maps api json response will look something like this
{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "18915",
               "short_name" : "18915",
               "types" : [ "postal_code" ]
            },
            {
               "long_name" : "Colmar",
               "short_name" : "Colmar",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "Pennsylvania",
               "short_name" : "PA",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "United States",
               "short_name" : "US",
               "types" : [ "country", "political" ]
            }
         ],
         "formatted_address" : "Colmar, PA 18915, USA",
         "geometry" : {
            "bounds" : {
               "northeast" : {
                  "lat" : 40.2850580,
                  "lng" : -75.24317099999999
               },
               "southwest" : {
                  "lat" : 40.25940490,
                  "lng" : -75.2680050
               }
            },
            "location" : {
               "lat" : 40.27383790,
               "lng" : -75.25082290
            },
            "location_type" : "APPROXIMATE",
            "viewport" : {
               "northeast" : {
                  "lat" : 40.2850580,
                  "lng" : -75.24317099999999
               },
               "southwest" : {
                  "lat" : 40.25940490,
                  "lng" : -75.2680050
               }
            }
         },
         "types" : [ "postal_code" ]
      }
   ],
   "status" : "OK"
}
I chose the formated_address field and split it up for ease of use.
Heres a sample of it working