Using the jQuery Store Locator with thousands of locations
A couple of months ago I assisted someone with a paid request to implement my jQuery Store Locator plugin with a database of over 50,000 locations. I hadn’t previously done this and wanted to share the solution I came up with for the benefit of anyone else who might be wondering how to proceed in the same situation. The key, of course, is to integrate with a back-end language but coming up with the appropriate database query that can quickly filter through lots of locations was somewhat tricky. In the newest version of my plugin I’ve added the appropriate parameters to the GET request now that I understand what’s involved: the origin latitude and longitude and I’m also passing the input that the user enters if you’re interested in logging that information for tracking purposes.
The answer that worked for me actually came right out of the O’Reilly book High Performance MySQL. PostgreSQL is actually much better for geospatial calculations but in my case it was an existing MySQL database and it would have been inconvenient to move it to something else. That said, I don’t think the following technique would be and issue with any database management system. Typically, nearest location calculations are done by creating a large sphere around the area (haversine formula) but if you try to attempt that with MySQL it really slows down the system and the performance is not reasonable at all. The trick is to pretend the earth is flat and just use a calculation that creates a large square around the potential area. The jQuery plugin does the haversine calculation, so the locations you feed it from the database don’t need to be extremely accurate. Here are the steps that you can follow – the code examples are in PHP but it should be easy to translate to other languages if needed.
The first step is to make sure the locations database tables are indexed.
Next, you’ll want to validate and sanitize parameter values that are passed over from the jQuery plugin.
if ( $_GET['origLat'] ) { $origLat = filter_input(INPUT_GET, 'origLat', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); } if ( $_GET['origLng'] ) { $origLng = filter_input(INPUT_GET, 'origLng', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); }
Next comes the database query that creates the gigantic square around the potential area. The following creates a 100 mile square around the origin location (0.0253 radians). You’ll want to increase the LIMIT depending on the density of locations. The best way to test this is to test addresses or zip codes with the most locations in an area – usually this will be large cities such as New York or Los Angeles. In testing I upped my limit to as much as 1,000 and didn’t see any significant performance issues but the lower you can make it the better. The following is a MySQLi query (the DB connection information is not included):
$locations = $db->query(" SELECT * FROM locations WHERE lat BETWEEN $origLat - DEGREES(0.0253) AND $origLat + DEGREES(0.0253) AND lng BETWEEN $origLng - DEGREES(0.0253) AND $origLng + DEGREES(0.0253) LIMIT 100 ");
Then all you need to do is loop through the results of the query and output the JSON data.
$locations_output = array(); foreach( $locations as $location ){ array_push( $locations_output, $location ); } echo json_encode( $locations_output );
When you’re adding the store locator plugin settings don’t forget to change the dataType to json instead of the default xml.
Oops! We could not locate your form.
Hi there Bjorn, I found it very surprising that you were asked to do something similar to a task (a large amount of addresses to store and query) I’ve been asked to do.
This is more like picking your brain if you don’t mind. I am not a pro PHP/JS coder, but I can get done what I need to most of the time.
I have a client who has about 40,000 previous customers they want to use in a “customer locator”. By this, I mean they want to have potential customers who are viewing their website, enter in their address and click “find customers near me”. The results would load the locations (customer addresses) within X amount of miles to the address they entered and plot them onto a map. In theory, it’s pretty much what you’ve already outlined in your scripts (which we already use a store locator).
Our current store locator is using a small locations.xml file with like 5 store locations. Each location I manually geocoded the lat/longs and with the other info, entered into the XML file.
For the “customers near me” search tool, we don’t want to display any popup info, or anything with name, address, etc, we only want to display markers to plot the locations so the potential customer can see the people within 1-5 miles of their address that are current or past customers of my client. They claim they’ve used this type of thing in the past, and was a great sales tool. What happened to their old “tool” I have no idea.
So, I have a spreadsheet with 10,000 customers (they sent it to me to work with initially) that I need to:
1) batch geocode the addresses to lat/long
2) store this into a Mysql database (only lat/long values, they don’t want actual street addresses, name, etc included)
The above two tasks I am pretty confident I can do without issue.
What I would like help with, is using your locator scripts to query the database once an address is searched on, then return the results into a .json file to plot the locations onto a map.
I am sorry if this doesn’t all make sense.
Any help would be appreciated.
Thanks,
Jeff
It sounds like you may be able to just follow what I’ve outlined above. Something similar to the code above (probably just need to modify the query to match your database tables) would be all you need to output the JSON – just point the locator plugin at the PHP file with the dataLocation setting and change the dataType to ‘json’.
Thanks Bjorn. After re-reading your initial post, and my reply, I realized you outlined just about everything I needed. I sent you a donation. Actually I asked you a question on your contact form. Maybe it’s something you want to answer here in case it helps someone else out. Thanks again, you’re saving me allot of time.
Good idea. I haven’t used any bulk geocoding services yet but have some links for you. The thing you will need to watch out for with these services is that a few, such as deCarta, will not let you store the results for future use, which doesn’t make much sense to me – be sure to check the TOS. I’ve seen recommendations for ArcGIS, which is supposed to be one of the best premium ones from my understanding but it’s probably going to be about the same cost as the one you found. You can also purchase a license for their software to use locally instead of the API but it’s pricey. For lists of services there’s a comparison on the Texas A&M Geoservices site and a Wikipedia list but I’m not sure either are completely up-to-date. Since you’re using US data another thing you could do would be to use the Census TIGER data in a local database, which would be free but requires some setup. My understanding with that method is you would download the national data and then use OGR2OGR to convert it to the format you want it in (PostGIS/PostgreSQL, MySQL, ODBC, SQLite among some other formats).
One good resource for comparing services and this kind of info is gis.stackexchange.com. You might also look through these questions.
Thanks for the donation!
Hi Bjorn,
I do have a support request after all.
I am having issues with:
$origLat = $_GET[“origLat”];
$origLng = $_GET[“origLng”];
It seems these values don’t get passed to the PHP script that makes the database query. If I manually add the var names and values to the query string of my page(?origLat=xxx&origLng=XXX), it then queries the database, and writes the results to my json file as well as shows the results on the map.
I noticed you mentioned in your post that “I’ve added the appropriate parameters to the GET request…” I am using your latest version of your plugin/files and am stuck on this. I saw some references to an ajax call in your plugin file, but I don’t see any documentation, or settings for this.
Do you think you can point me in the right direction? I can post my code if it will help.
Oh, and thanks for the links for the batch geocoding. My client says they will be sending me a spreadsheet once a month with their “new” customers that I will have to convert to CSV and then batch geocode to lat/lng, then I plan on manually adding the rows to the database via phpmyadmin.
Thanks!
Jeff
It’s set up to pass the parameters by default. If you open Chrome DevTools or another equivalent and enable “Log XMLHttpRequests” in the console settings you can view the AJAX requests. If you look at the examples I have set up like the default one, you’ll see the parameters in the third GET request (first two are the templates). What does it show for you?
Thanks Bjorn,
I was confused on the way the results were being called and displayed. I thought the db query had to actually be written to the .json file. Once I saw the ajax call to my data file, I realized the info was there, I just needed to use it right. This is pretty new to me, but a good learning experience.
Jeff
Hi Bjorn,
Loving this plug-in as it so beautifully simplifies what Google has made really complex. I have an issue similar to Jeff’s in that I’m querying a rather large database to return limited results via JSON (which works perfectly once a query is made via the form). However, I’m using autoGeocode (true) to initialize the map and call my data via a PHP file. On that first AJAX call to my dataLocation, only the origAddress parameter is sent in the querystring, but not origLat or origLng. I have tried various combinations of setting defaultLoc, defaultLat, defaultLng to see if I could force them to be sent on the first request, but nothing seems to work.
The reason I want this is so when visitors first come to the map and they have their browser’s geolocation enabled, I can return my results limited to a distance from their location via my db query (which I can only do if lat and lng are supplied).
So the question is, how can I get the plug-in to add origLat and origLng to the query on that first run when autoGeocode is true?
Thanks!
Looked into this issue and it’s a bug. The global olat and olng variables aren’t being set in the autoGeocodeQuery method that starts on 945. I’m going to release an update soon but you can use the following in the meantime to replace line 954 and 955:
Hey Bjorn,
thanks a lot for all your work, just awesome some people like you can be good in a work 😀
I worked on your plugin for some hours and wanted to see the json from a php file and with defaultloc with defaultlat and defaultlng… having a map when people come on the web site but i was blocked by ?origaddress only and nothing going by the GET procedure…
it was not working together, so after lots of reflexion I tried to change with the solution you just explained in this post for “autogeocode” and the final code to fix the bug is :
_start: function () {
var _this = this;
var mappingObj = {};
// If a default location is set
// The address needs to be deermined for the directions link
if (this.settings.defaultLoc === true) {
var r = new this.reverseGoogleGeocode(this);
var latlng = new google.maps.LatLng(this.settings.defaultLat, this.settings.defaultLng);
r.geocode({‘latLng’: latlng}, function (data) {
if (data !== null) {
var originAddress = data.address;
olat = mappingObj.lat = _this.settings.defaultLat;
olng = mappingObj.lng = _this.settings.defaultLng;
mappingObj.origin = originAddress;
_this.mapping(mappingObj);
} else {
// Unable to geocode
_this.notify(_this.settings.addressErrorAlert);
}
});
}
I hpe everybody understood, but I hope it will be helpful for somebody…
thanks a lot one more time for your work,
Ben from France !
Hi Bjorn,
This is a great script, really hepful.
Is there any setting that would list the loc-list on the left alphabetically, rather than by distance from the map centre?
Regards,
Phil
UPDATE
Hi,
It’s OK, I figured it out, I simply changed the “distance” to “name” in the
function sort_numerically
part of the jquery.storelocator.js Hope this is OK?
Phil
Bjorn,
I am hoping you can point me in the right direction for this next question I have.
My client has six store locations (Ohio, Kentucky, and Illinois). They only service customers within 50 miles of their 6 store locations. If I was only using this plugin as a “store” locator, this wouldn’t be an issue. Since I’ve been working on the customer locator, when people enter in an address, the query results are related to customer locations, so 50 miles from a customer can be much further than 50 miles away from one of their stores. I would like the search result to only show customers within a 50 mile radius of a store location(the store locations can be an array of static lat/lng values), and if there are no stores within a 50 mile radius, I want to display the alert prompt for maxdistance, then plot all six store locations along with the origin marker of the searched address.
Being pretty new to client side scripting, I am chasing my tail with this.
Any help would be appreciated. I will be more than happy to donate for your time once again if you can give me some direction.
Thanks,
Jeff
The first part of this should be pretty easy – you can just use the maxDistance setting and have it permanently set to 50 in a hidden input field in the main form.
For the other part, I did recently add a callback in v2.0.2 for no results (callbackNoResults) and you might be able to get by using that by updating the settings. I would try using that callback to update the settings so that the dataLocation (_this.settings.dataLocation) was changed to a separate data file with the 6 locations, then setting the originMarker setting to true, then run the mapping function again. I would have to do some testing to see if that would work. I’m not positive if you can reassign the settings in a callback.
Actually, I just pushed v2.0.3 up with a bug fix dealing with the maxDistance setting so you’ll want to use at least that version.
The thousand markers are appearing. To make it look pretty I have been trying to use MarkerCluster without success. Any pointers?
Check the query limit and the storeLimit option in the plugin if you’re wanting less to display. I haven’t done anything with clustering yet but someone mentioned that they implemented something basic on GitHub: https://github.com/bjorn2404/jQuery-Store-Locator-Plugin/issues/33
That worked! Thanks!
Nice code Bjorn.
Is it possible to see an example with thousands of location loaded ?
Thank you!!
No, not really because the thousands would be loaded into a database and not the plugin itself and I’m not going to be able to display a live database.
Is it possible to e-mail or upload a working example (with sql dump of a few locations)? I’m so lost on how to start this….
Indeed a great plugin Bjorn
Does anyone know why the plugin stops working in Safari ? I keep getting error the plugin template cannot be loaded: ” Error: Could not load plugin templates. Check the paths and ensure they have been uploaded. Paths will be wrong if you do not run this from a web server”. I can;t figure out how and why Safari is blocking the template.
It used to work in Safari , but suddenly it stopped . It still works in Chrome and Firefox.
Any insight will be appreciated
Having this issue as well. Seems to only affect specific users too for some reason… But, definitely somehow connected with Safari.
Does it happen consistently? What version of Safari? The templates are called via AJAX requests that should fire immediately on the page load. If you can replicate it consistently and check the console to view the AJAX requests that would be helpful. If there’s nothing obvious I would suggest using inline Handlebar templates with the listTemplateID and infowindowTemplateID settings. The second example on the Handlebars website is how you would do the inline templates (copy from the included template files and set the ID). Otherwise, the only thing I’m seeing is a suggestion to set async to true in the AJAX requests. To do that you’d have to change the shorthand $.get functions to full $.ajax functions in the loadTemplates method of the plugin.
Hi, i changed it but it does not help. Still the same error.
Btw, i have the problems in the current version of FF (38.0.1) and the FF Developer Ed. (40.0a2 (2015-05-26)) and in the Chrome (Version 43.0.2357.81 m).
Just in the IE11 XD it works…. WTF? 🙂
Please Help!
how do i implement the zoom filter in category demo, so when i zoom into an area the side bar just display whats close
I got everything working thanks to your instructions, only thing i am experiencing is slow zoom load because of the amount of points in the screen, is there any solution about maybe storing in a cookie or separating the points in different files. please advice
Hi,
The plugin seems like an all-in-one swiss knife. However I am not able to get it to work. I downloaded and unzipped the folder from github. But when I open and of the “dist” examples I get the error
“Error: Could not load plugin templates. Check the paths and ensure they have been uploaded. Paths will be wrong if you do not run this from a web server.”
I tried to run the tool from jsfiddle but faced the same issue.
Will appreciate If you can guide me the way to fix it.
Thanks,
Sandeep
Same Problem…
latest FF and Chrome! IE11 it works 🙂
Hi,
I am using your plugin. it’s working perfect.
Thanks for such a fantastic plugin.
Here is a link of my site.
http://www.watchoogle.com/find-a-retailer/
I want to show all location default when there is no brand or country selected.
How can i do ?
i really don’t know jquery 🙁
i am using xml file to add locations.
http://www.watchoogle.com/wp-content/uploads/2015/06/listofjune.xml
Instead of this message i want to show all location.
( “No results, No retailer/locations were found with the given criteria. Please modify your selections.”)
Thanks
Wajid
hi, I have a map with more than thousand locations, I notice that the slowest thing is the recalculation of the locations lists display in the map (if I set visibleMarkersList to false, the performances are not bad, but it’s not the optimum solution for my customer). Will the performances improve if I use precompiled Handlebars templates? If so, how can I do that?
The point of the post above was to emphasize that a database is best when you’re dealing with lots of locations so you’re not feeding the jQuery plugin so much data. If you’re already doing this I’d recommend trying inline templates with the listTemplateID and infowindowTemplateID options. Pre-compiling details can be found on the Handlebars website: http://handlebarsjs.com/precompilation.html
Hi Bjorn,
Thx for this wonderfull script!
I was wondering what the best way would be to bulk auto-geocode the locations. Do you have an example for that?
I’ve read your blogpost but how did you deal with 50000 adresses and the longitude and latitude data? Did you use some free/paid API for that?
Or I’m I completly missing something and this script does everything automatically??
The plugin does not do geocoding of the location data automatically, just the user’s location. The data I worked with was already geocoded and you want to do all the geocoding of the locations prior to any searches because doing it on the fly would be a bandwidth concern and the costs could add up quickly if you were using an API. I haven’t used anything outside of the Google and Bing APIs so I can’t give you a solid recommendation but I’ve seen some information that would lead me to believe that ArcGIS is a good service. The Texas A&M Geoservices site has a service comparison but I’m not sure how up to date it is. There are also other options like downloading the TIGER census data but that depends on where your locations are located (good for US – not sure about other countries). Last, I’d recommend searching the GIS stackexchange or asking for recommendations there.
Hi Bjorn,
I’ve been working on another project (same client) and have put your plugin to good use once again. I’ve added more columns to the DB that the client needs queried per location (this is a customer and store locator). One of these columns is “sale_date” (mysql DATE). In my mysql query, after it finds all the locations in the given radius, I am then filtering the results by a date range (start_date, finish_date) and ordering my results via DESC . The json output from my query is ordered correctly, but when it gets processed by your script, it’s being sorted by distance. I now need to conditionally list the results by distance, or date range depending on the query.
In you locator script, I found:
/**
* Location distance sorting function
*
* @param locationsarray {array} locationset array
*/
sortNumerically: function (locationsarray) {
this.writeDebug(‘sortNumerically’,arguments);
locationsarray.sort(function (a, b) {
return ((a.distance b.distance) ? 1 : 0));
});
},
Do you have any suggestions for me to pass the sale_date value to your script and then sort by date instead of distance? The reason is that the location listing that I have placed below the map will have about 10 columns per row with customer information (name, addy, phone, cell, customer #, notes, email address, etc, etc).
Glad to see you’re still keeping this project alive.
Thanks in advance,
Jeff
Any help will be appreciated.
Hi Jeff,
How do you have the search form set up? Is there an option to choose distance or date before the search is performed?
Hi Bjorn,
Yes, I have a customer limit, radius, start date, and finish date that is all sent to the script that queries the DB. If the script gets a date range, it checks the dates, then distance from queried address and outputs json to the plugin.
If I look at the returned json, the results are ordered by date (ascending), but when the plugin processes the json, it re-orders by distance.
Thanks!
Jeff
Here’s an example of the returned json:
Hi again Bjorn,
I managed to come up with a pretty simple solution to this. I found if by-passing the sortNumerically function, that it left the array unchanged.
I added an option to your plugin that essentially bypasses the sorting by distance when needed.
Thanks,
Jeff
Great! Glad you were able to figure it out.
Do you have any support for overlaying a KML layer, I have a kml layer with “lines” drawn around certain locations that id like to overlay and eventually have it part of the search. Ie a user enters their address and it returns what “region” they are in based on the kml layer and what store is closest to them.
I’m trying to figure out how to filter results by clicking multiple categories at the same time. Can’t get that to work for some reason. I have 3,000 locations.
Excellent plugin. Congratulations.
I am a beginner in javascript
I’m trying to several days to make the search by name ignore accents.
Where I can get the word with or without the accent and she find.
Has anyone managed to do this?
And could give a hint?
Not sure if this is my site Bjorn is referring to but I can tell you the implementation he did for me works awesome! It’s pointing now to 65,000 stores and works perfect and isn’t a page load hog whatsoever. I recently redirected the site from www to non-www and added an SSL certificate. After breaking the application I went back to some emails we exchanged and found the issue and it’s back to working great.
I do see one item, Bjorn, that I’m not sure about. Now that it’s working again it seems as though the locator is off by about 10 miles. I used it see it open up right in my neighborhood (or within a mile or so), now it’s off by, as I said, 5-10 miles.
Thoughts? I’ll follow up with an email, too.
I cannot recommend Bjorn enough. Very impressed with the initial implementation and the post-implantation support. #bigFan
Hi Bjorn,
Absolutely fantastic post, 10 out of 10 on this one mate, To be honest iv been learning as much as i possibly can 7 days a week this last few weeks on geographic web development and working with google API JavaScript frontend served by laravel back-end.
I have not managed to get any formula working as quick as the one you have posted above, I’m serving over 25 Million records in a MySQL database to this formula with some slight MySQL tweaks for UK locations only and the accuracy is fairly bang on.. I will be switching to redis to leverage their geospatial functionality soon but in the mean time this has massively helped 🙂
Will donate as soon as possible, Really appreciate that you took the time to help us all out, Thank you.
Where can I get help on dynamically displaying new markers from a database as I drag the map with my mouse?
Hi having problem with No locations found. Please check the dataLocation setting and path. I already updated the dataLocation Path ‘dataLocatio’ : ‘../map/personnel.xml’, but I’m still having the error
Hi,
Very nice work, thank a lot.
When I use the “catMarkers” it works fine, but if I put a town where no locations exists,
the message ” no location here, try something other” doesn’t appear.
What can I do ?
Thanks.
P.S.: please, excuse my bad english.
I am trying to use this plugin and have a visitor fill out their home address with this form:
<form action="proximity_finder_map.php?bh-sl-address=” id=”bh-sl-user-location” method=”get” name=”form1″>
Address
City
Zip Code
Grade
Montessori (3 yrs old)
Montessori (4 yrs old)
Junior Kindergarten
Kindergarten
Grade 1
Grade 2
Grade 3
Grade 4
Grade 5
Grade 6
Grade 7
Grade 8
Which then redirects to this page:
setQuery(”
SELECT * FROM schools left join geocodes on schools.map_geocode_id = geocodes.geocode_id
WHERE lat BETWEEN $origLat – DEGREES(0.0253) AND $origLat + DEGREES(0.0253)
AND lng BETWEEN $origLng – DEGREES(0.0253) AND $origLng + DEGREES(0.0253)
LIMIT 100
“);
$locations->execute();
$locations_output = array();
foreach( $locations as $location ){
array_push( $locations_output, $location );
}
echo json_encode( $locations_output );
?>
CPS: School Proximity Finder
ul.nav li a, ul.nav li a:visited {
color: #375d8b !important;
}
ul.nav li a:hover, ul.nav li a:active {
color: #375d8b !important;
}
ul.nav li.active a {
color: #375d8b !important;
}
p {
font-family: “Open Sans”, Helvetica, sans-serif;
font-size: 15px;
line-height: 1.4;
}
.footer-font {
color: #FFF;
font-family: “Open Sans”, Helvetica, sans-serif;
text-decoration:none;
line-height:17px;
}
.footer_titles {
color: #CCC;
font-family: “Open Sans”, Helvetica, sans-serif;
font-size:12px;
}
.nav { margin-bottom: 0;
margin-top: 0;
}
Cambidge Public Schools
Student Registration Center
Kindergarten Lottery
Email the SRC
School Proximity Finder Map Results
http://www.cpsd.us
$(function () {
$(‘#bh-sl-map-container’).storeLocator({
dataType: ‘json’,
‘querystringParams’ : true,
‘fullMapStart’: true,
‘mapSettings’: {
zoom : 12,
mapTypeId : google.maps.MapTypeId.ROADMAP,
disableDoubleClickZoom: true,
scrollwheel : false,
navigationControl : false,
draggable : false
}
});
});
What should happen is based on the user’s input of their home address, the map should display the closest 3 schools to them from the database. Any suggestions on what I am doing wrong? Thanks
How are you storing the coordinates in the database? GIS types or DECIMAL?
Hi
I want to search in search box Name of title and Address both but no any solution find