There's Geo Data Store by our own man, @Brady. It does a perfect job in one of my installations.
It uses a custom table, that stores only ID, meta ID, lat and lng information, so your queries stay quick and brief, even when your database starts scaling. It got a 5 star rating from my side.
This plugin is meant to be used by other developers and to be used together with themes and other plugins. Many themes and plugins use WordPress meta data table to store longitude and latitude co-ordinates for posts. While this works fine the meta data table cannot be indexed very well. Let's take for example you have made a custom post type called 'properties'. You create 100,000 posts all attached with latitude and longitude co-ordinates. You want your users to search for those properties in a 50 mile radius for example. Because of the method of which WordPress stores the meta data the query is slow especially when dealing with large amounts of data.
@Brady in the plugin description.
Note: There's a bug - related to WP internals - that rows don't delete, when you delete a post. Don't be concerned about it. I tried to work through it with the dev and we didn't get behind it. Anyway: It doesn't harm anything, but you should clean up your tables, before you go live (or test in a local installation), to minimize the amount of non related rows.
Sample code from Scotts pastebin in the comments
// Only do a map search if user submitted one
if( 2 == $_SESSION['s_doing_property_search'] )
// Only generate map search results if we don't have any yet or if they are a day out of date
if( empty( $_SESSION['homes_for_' . $type . '_map_search_results'] ) || $_SESSION['homes_for_' . $type . '_map_search_timestamp'] < strtotime( "-1 Day" ) )
// Load instance of GeoDataStore
if ( ! isset( $sc_gds ) )
$sc_gds = new sc_GeoDataStore();
// Just get the ID's of posts in range
$_SESSION['homes_for_' . $type . '_map_search_results'] = (array) $sc_gds->getPostIDsOfInRange( "homes-for-" . $type, $_SESSION['s_property_radius'], $_SESSION['s_property_address_lat'], $_SESSION['s_property_address_lng'] );
// Set a timestamp so we don't do this expensive get each page load.
$_SESSION['homes_for_' . $type . '_map_search_timestamp'] = time();
// We we have no results then set an array just one that will trigger no posts found.
if( empty( $_SESSION['homes_for_' . $type . '_map_search_results'] ) )
$query->set( 'post__in', array(1) );
$query->set( 'post__in', $_SESSION['homes_for_' . $type . '_map_search_results'] );