]> git.openstreetmap.org Git - nominatim.git/blob - website/search.php
support for partial matching of US ZIP+4 codes
[nominatim.git] / website / search.php
1 <?php
2         @define('CONST_ConnectionBucket_PageType', 'Search');
3
4         require_once(dirname(dirname(__FILE__)).'/lib/init-website.php');
5         require_once(CONST_BasePath.'/lib/log.php');
6
7         ini_set('memory_limit', '200M');
8
9         $oDB =& getDB();
10
11         // Display defaults
12         $fLat = CONST_Default_Lat;
13         $fLon = CONST_Default_Lon;
14         $iZoom = CONST_Default_Zoom;
15         $bBoundingBoxSearch = isset($_GET['bounded'])?(bool)$_GET['bounded']:false;
16         $sOutputFormat = 'html';
17         $aSearchResults = array();
18         $aExcludePlaceIDs = array();
19         $sCountryCodesSQL = false;
20         $bDeDupe = isset($_GET['dedupe'])?(bool)$_GET['dedupe']:true;
21         $bReverseInPlan = false;
22         $iFinalLimit = isset($_GET['limit'])?(int)$_GET['limit']:10;
23         $iOffset = isset($_GET['offset'])?(int)$_GET['offset']:0;
24         $iMaxRank = 20;
25         if ($iFinalLimit > 50) $iFinalLimit = 50;
26         $iLimit = $iFinalLimit + min($iFinalLimit, 10);
27         $iMinAddressRank = 0;
28         $iMaxAddressRank = 30;
29         $aAddressRankList = array();
30         $sAllowedTypesSQLList = false;
31
32         // Format for output
33         if (isset($_GET['format']) && ($_GET['format'] == 'html' || $_GET['format'] == 'xml' || $_GET['format'] == 'json' ||  $_GET['format'] == 'jsonv2'))
34         {
35                 $sOutputFormat = $_GET['format'];
36         }
37
38         // Show / use polygons
39         $bShowPolygons = (boolean)isset($_GET['polygon']) && $_GET['polygon'];
40         if ($sOutputFormat == 'html')
41         {
42                 $bAsText = $bShowPolygons;
43                 $bShowPolygons = false;
44                 $bAsGeoJSON = false;
45                 $bAsKML = false;
46                 $bAsSVG = false;
47         }
48         else
49         {
50                 $bAsGeoJSON = (boolean)isset($_GET['polygon_geojson']) && $_GET['polygon_geojson'];
51                 $bAsKML = (boolean)isset($_GET['polygon_kml']) && $_GET['polygon_kml'];
52                 $bAsSVG = (boolean)isset($_GET['polygon_svg']) && $_GET['polygon_svg'];
53                 $bAsText = (boolean)isset($_GET['polygon_text']) && $_GET['polygon_text'];
54                 if ((($bShowPolygons?1:0)
55                                         + ($bAsGeoJSON?1:0)
56                                         + ($bAsKML?1:0)
57                                         + ($bAsSVG?1:0)
58                                         + ($bAsText?1:0)
59                         ) > CONST_PolygonOutput_MaximumTypes)
60                 {
61                         if (CONST_PolygonOutput_MaximumTypes)
62                         {
63                                 userError("Select only ".CONST_PolygonOutput_MaximumTypes." polgyon output option");
64                         }
65                         else
66                         {
67                                 userError("Polygon output is disabled");
68                         }
69                         exit;
70                 }
71         }
72
73         // Show address breakdown
74         $bShowAddressDetails = isset($_GET['addressdetails']) && $_GET['addressdetails'];
75
76         // Preferred language
77         $aLangPrefOrder = getPreferredLanguages();
78         if (isset($aLangPrefOrder['name:de'])) $bReverseInPlan = true;
79         if (isset($aLangPrefOrder['name:ru'])) $bReverseInPlan = true;
80         if (isset($aLangPrefOrder['name:ja'])) $bReverseInPlan = true;
81
82         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$aLangPrefOrder))."]";
83
84         if (isset($_GET['exclude_place_ids']) && $_GET['exclude_place_ids'])
85         {
86                 foreach(explode(',',$_GET['exclude_place_ids']) as $iExcludedPlaceID)
87                 {
88                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
89                         if ($iExcludedPlaceID) $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
90                 }
91         }
92
93         // Only certain ranks of feature
94         if (isset($_GET['featureType']) && !isset($_GET['featuretype'])) $_GET['featuretype'] = $_GET['featureType'];
95
96         if (isset($_GET['featuretype']))
97         {
98                 switch($_GET['featuretype'])
99                 {
100                 case 'country':
101                         $iMinAddressRank = $iMaxAddressRank = 4;
102                         break;
103                 case 'state':
104                         $iMinAddressRank = $iMaxAddressRank = 8;
105                         break;
106                 case 'city':
107                         $iMinAddressRank = 14;
108                         $iMaxAddressRank = 16;
109                         break;
110                 case 'settlement':
111                         $iMinAddressRank = 8;
112                         $iMaxAddressRank = 20;
113                         break;
114                 }
115         }
116
117         if (isset($_GET['countrycodes']))
118         {
119                 $aCountryCodes = array();
120                 foreach(explode(',',$_GET['countrycodes']) as $sCountryCode)
121                 {
122                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
123                         {
124                                 $aCountryCodes[] = "'".strtolower($sCountryCode)."'";
125                         }
126                 }
127                 $sCountryCodesSQL = join(',', $aCountryCodes);
128         }
129
130         // Search query
131         $sQuery = (isset($_GET['q'])?trim($_GET['q']):'');
132         if (!$sQuery && isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'][0] == '/')
133         {
134                 $sQuery = substr($_SERVER['PATH_INFO'], 1);
135
136                 // reverse order of '/' separated string
137                 $aPhrases = explode('/', $sQuery);
138                 $aPhrases = array_reverse($aPhrases);
139                 $sQuery = join(', ',$aPhrases);
140         }
141
142         // Structured query?
143         $aStructuredOptions = array(
144                                 array('amenity', 26, 30, false),
145                                 array('street', 26, 30, false),
146                                 array('city', 14, 24, false),
147                                 array('county', 9, 13, false),
148                                 array('state', 8, 8, false),
149                                 array('country', 4, 4, false),
150                                 array('postalcode', 5, 11, array(5, 11)),
151                                 );
152         $aStructuredQuery = array();
153         $sAllowedTypesSQLList = '';
154         foreach($aStructuredOptions as $aStructuredOption)
155         {
156                 loadStructuredAddressElement($aStructuredQuery, $iMinAddressRank, $iMaxAddressRank, $aAddressRankList, $_GET, $aStructuredOption[0], $aStructuredOption[1], $aStructuredOption[2], $aStructuredOption[3]);
157         }
158         if (sizeof($aStructuredQuery) > 0) 
159         {
160                 $sQuery = join(', ', $aStructuredQuery);
161                 if ($iMaxAddressRank < 30)
162                 {
163                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
164                 }
165         }
166
167         if ($sQuery)
168         {
169                 $hLog = logStart($oDB, 'search', $sQuery, $aLangPrefOrder);
170
171                 // Hack to make it handle "new york, ny" (and variants) correctly
172                 $sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $sQuery);
173                 if (isset($aLangPrefOrder['name:en']))
174                 {
175                         $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
176                         $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
177                         $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
178                 }
179
180                 // If we have a view box create the SQL
181                 // Small is the actual view box, Large is double (on each axis) that
182                 $sViewboxCentreSQL = $sViewboxSmallSQL = $sViewboxLargeSQL = false;
183                 if (isset($_GET['viewboxlbrt']) && $_GET['viewboxlbrt'])
184                 {
185                         $aCoOrdinatesLBRT = explode(',',$_GET['viewboxlbrt']);
186                         $_GET['viewbox'] = $aCoOrdinatesLBRT[0].','.$aCoOrdinatesLBRT[3].','.$aCoOrdinatesLBRT[2].','.$aCoOrdinatesLBRT[1];
187                 }
188                 if (isset($_GET['viewbox']) && $_GET['viewbox'])
189                 {
190                         $aCoOrdinates = explode(',',$_GET['viewbox']);
191                         $sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aCoOrdinates[0].",".(float)$aCoOrdinates[1]."),ST_Point(".(float)$aCoOrdinates[2].",".(float)$aCoOrdinates[3].")),4326)";
192                         $fHeight = $aCoOrdinates[0]-$aCoOrdinates[2];
193                         $fWidth = $aCoOrdinates[1]-$aCoOrdinates[3];
194                         $aCoOrdinates[0] += $fHeight;
195                         $aCoOrdinates[2] -= $fHeight;
196                         $aCoOrdinates[1] += $fWidth;
197                         $aCoOrdinates[3] -= $fWidth;
198                         $sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aCoOrdinates[0].",".(float)$aCoOrdinates[1]."),ST_Point(".(float)$aCoOrdinates[2].",".(float)$aCoOrdinates[3].")),4326)";
199                 }
200                 else
201                 {
202                         $bBoundingBoxSearch = false;
203                 }
204                 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
205                 {
206                         $aPoints = explode(',',$_GET['route']);
207                         if (sizeof($aPoints) % 2 != 0)
208                         {
209                                 userError("Uneven number of points");
210                                 exit;
211                         }
212                         $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
213                         $fPrevCoord = false;
214                         foreach($aPoints as $i => $fPoint)
215                         {
216                                 if ($i%2)
217                                 {
218                                         if ($i != 1) $sViewboxCentreSQL .= ",";
219                                         $sViewboxCentreSQL .= ((float)$fPoint).' '.$fPrevCoord;
220                                 }
221                                 else
222                                 {
223                                         $fPrevCoord = (float)$fPoint;
224                                 }
225                         }
226                         $sViewboxCentreSQL .= ")'::geometry,4326)";
227
228                         $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
229                         $sViewboxSmallSQL = $oDB->getOne($sSQL);
230                         if (PEAR::isError($sViewboxSmallSQL))
231                         {
232                                 failInternalError("Could not get small viewbox.", $sSQL, $sViewboxSmallSQL);
233                         }
234                         $sViewboxSmallSQL = "'".$sViewboxSmallSQL."'::geometry";
235
236                         $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
237                         $sViewboxLargeSQL = $oDB->getOne($sSQL);
238                         if (PEAR::isError($sViewboxLargeSQL))
239                         {
240                                 failInternalError("Could not get large viewbox.", $sSQL, $sViewboxLargeSQL);
241                         }
242                         $sViewboxLargeSQL = "'".$sViewboxLargeSQL."'::geometry";
243                         $bBoundingBoxSearch = true;
244                 }
245
246                 // Do we have anything that looks like a lat/lon pair?
247                 if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
248                 {
249                         $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
250                         $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
251                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
252                         {
253                                 $_GET['nearlat'] = $fQueryLat;
254                                 $_GET['nearlon'] = $fQueryLon;
255                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
256                         }
257                 }
258                 elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
259                 {
260                         $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
261                         $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
262                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
263                         {
264                                 $_GET['nearlat'] = $fQueryLat;
265                                 $_GET['nearlon'] = $fQueryLon;
266                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
267                         }
268                 }
269                 elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9.]*)[, ]+(-?[0-9]+[0-9.]*)(\\]|$|\\b)/', $sQuery, $aData))
270                 {
271                         $fQueryLat = $aData[2];
272                         $fQueryLon = $aData[3];
273                         if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
274                         {
275                                 $_GET['nearlat'] = $fQueryLat;
276                                 $_GET['nearlon'] = $fQueryLon;
277                                 $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
278                         }
279                 }
280
281                 if ($sQuery || $aStructuredQuery)
282                 {
283                         // Start with a blank search
284                         $aSearches = array(
285                                 array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
286                                       'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
287                                       'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
288                         );
289
290                         $sNearPointSQL = false;
291                         if (isset($_GET['nearlat']) && isset($_GET['nearlon']))
292                         {
293                                 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$_GET['nearlon'].",".(float)$_GET['nearlat']."),4326)";
294                                 $aSearches[0]['fLat'] = (float)$_GET['nearlat'];
295                                 $aSearches[0]['fLon'] = (float)$_GET['nearlon'];
296                                 $aSearches[0]['fRadius'] = 0.1;
297                         }
298
299                         $bSpecialTerms = false;
300                         preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
301                         $aSpecialTerms = array();
302                         foreach($aSpecialTermsRaw as $aSpecialTerm)
303                         {
304                                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
305                                 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
306                         }
307
308                         preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
309                         $aSpecialTerms = array();
310                         if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
311                         {
312                                 $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
313                                 unset($aStructuredQuery['amenity']);
314                         }
315                         foreach($aSpecialTermsRaw as $aSpecialTerm)
316                         {
317                                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
318                                 $sToken = $oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
319                                 $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
320                                 $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
321                                 if (CONST_Debug) var_Dump($sSQL);
322                                 $aSearchWords = $oDB->getAll($sSQL);
323                                 $aNewSearches = array();
324                                 foreach($aSearches as $aSearch)
325                                 {
326                                         foreach($aSearchWords as $aSearchTerm)
327                                         {
328                                                 $aNewSearch = $aSearch;
329                                                 if ($aSearchTerm['country_code'])
330                                                 {
331                                                         $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
332                                                         $aNewSearches[] = $aNewSearch;
333                                                         $bSpecialTerms = true;
334                                                 }
335                                                 if ($aSearchTerm['class'])
336                                                 {
337                                                         $aNewSearch['sClass'] = $aSearchTerm['class'];
338                                                         $aNewSearch['sType'] = $aSearchTerm['type'];
339                                                         $aNewSearches[] = $aNewSearch;
340                                                         $bSpecialTerms = true;
341                                                 }
342                                         }
343                                 }
344                                 $aSearches = $aNewSearches;
345                         }
346
347                         // Split query into phrases
348                         // Commas are used to reduce the search space by indicating where phrases split
349                         if (sizeof($aStructuredQuery) > 0)
350                         {
351                                 $aPhrases = $aStructuredQuery;
352                                 $bStructuredPhrases = true;
353                         }
354                         else
355                         {
356                                 $aPhrases = explode(',',$sQuery);
357                                 $bStructuredPhrases = false;
358                         }
359
360                         // Convert each phrase to standard form
361                         // Create a list of standard words
362                         // Get all 'sets' of words
363                         // Generate a complete list of all
364                         $aTokens = array();
365                         foreach($aPhrases as $iPhrase => $sPhrase)
366                         {
367                                 $aPhrase = $oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
368                                 if (PEAR::isError($aPhrase))
369                                 {
370                                         userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
371                                         if (CONST_Debug) var_dump($aPhrase);
372                                         exit;
373                                 }
374                                 if (trim($aPhrase['string']))
375                                 {
376                                         $aPhrases[$iPhrase] = $aPhrase;
377                                         $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
378                                         $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
379                                         $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
380                                 }
381                                 else
382                                 {
383                                         unset($aPhrases[$iPhrase]);
384                                 }
385                         }
386
387                         // reindex phrases - we make assumptions later on
388                         $aPhraseTypes = array_keys($aPhrases);
389                         $aPhrases = array_values($aPhrases);
390
391                         if (sizeof($aTokens))
392                         {
393
394                                 // Check which tokens we have, get the ID numbers
395                                 $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
396                                 $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
397                                 //$sSQL .= ' and search_name_count < '.CONST_Max_Word_Frequency;
398                                 //$sSQL .= ' group by word_token, word, class, type, country_code';
399
400                                 if (CONST_Debug) var_Dump($sSQL);
401
402                                 $aValidTokens = array();
403                                 if (sizeof($aTokens)) $aDatabaseWords = $oDB->getAll($sSQL);
404                                 else $aDatabaseWords = array();
405                                 if (PEAR::IsError($aDatabaseWords))
406                                 {
407                                         failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
408                                 }
409                                 $aPossibleMainWordIDs = array();
410                                 $aWordFrequencyScores = array();
411                                 foreach($aDatabaseWords as $aToken)
412                                 {
413                                         // Very special case - require 2 letter country param to match the country code found
414                                         if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
415                                                         && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
416                                         {
417                                                 continue;
418                                         }
419
420                                         if (isset($aValidTokens[$aToken['word_token']]))
421                                         {
422                                                 $aValidTokens[$aToken['word_token']][] = $aToken;
423                                         }
424                                         else
425                                         {
426                                                 $aValidTokens[$aToken['word_token']] = array($aToken);
427                                         }
428                                         if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
429                                         $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
430                                 }
431                                 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
432
433                                 // Try and calculate GB postcodes we might be missing
434                                 foreach($aTokens as $sToken)
435                                 {
436                                         // Source of gb postcodes is now definitive - always use
437                                         if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
438                                         {
439                                                 if (substr($aData[1],-2,1) != ' ')
440                                                 {
441                                                         $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
442                                                         $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
443                                                 }
444                                                 $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $oDB);
445                                                 if ($aGBPostcodeLocation)
446                                                 {
447                                                         $aValidTokens[$sToken] = $aGBPostcodeLocation;
448                                                 }
449                                         }
450                                         // US ZIP+4 codes - if there is no token,
451                                         //      merge in the 5-digit ZIP code
452                                         else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
453                                         {
454                                                 if (isset($aValidTokens[$aData[1]]))
455                                                 {
456                                                         foreach($aValidTokens[$aData[1]] as $aToken)
457                                                         {
458                                                                 if (!$aToken['class'])
459                                                                 {
460                                                                         if (isset($aValidTokens[$sToken]))
461                                                                         {
462                                                                                 $aValidTokens[$sToken][] = $aToken;
463                                                                         }
464                                                                         else
465                                                                         {
466                                                                                 $aValidTokens[$sToken] = array($aToken);
467                                                                         }
468                                                                 }
469                                                         }
470                                                 }
471                                         }
472                                 }
473
474                                 foreach($aTokens as $sToken)
475                                 {
476                                         // Unknown single word token with a number - assume it is a house number
477                                         if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
478                                         {
479                                                 $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
480                                         }
481                                 }
482
483                                 // Any words that have failed completely?
484                                 // TODO: suggestions
485
486                                 // Start the search process
487                                 $aResultPlaceIDs = array();
488
489                                 /*
490                                    Calculate all searches using aValidTokens i.e.
491                                    'Wodsworth Road, Sheffield' =>
492
493                                    Phrase Wordset
494                                    0      0       (wodsworth road)
495                                    0      1       (wodsworth)(road)
496                                    1      0       (sheffield)
497
498                                    Score how good the search is so they can be ordered
499                                  */
500                                 foreach($aPhrases as $iPhrase => $sPhrase)
501                                 {
502                                         $aNewPhraseSearches = array();
503                                         if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
504                                         else $sPhraseType = '';
505
506                                         foreach($aPhrases[$iPhrase]['wordsets'] as $aWordset)
507                                         {
508                                                 $aWordsetSearches = $aSearches;
509
510                                                 // Add all words from this wordset
511                                                 foreach($aWordset as $iToken => $sToken)
512                                                 {
513                                                         //echo "<br><b>$sToken</b>";
514                                                         $aNewWordsetSearches = array();
515
516                                                         foreach($aWordsetSearches as $aCurrentSearch)
517                                                         {
518                                                                 //echo "<i>";
519                                                                 //var_dump($aCurrentSearch);
520                                                                 //echo "</i>";
521
522                                                                 // If the token is valid
523                                                                 if (isset($aValidTokens[' '.$sToken]))
524                                                                 {
525                                                                         foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
526                                                                         {
527                                                                                 $aSearch = $aCurrentSearch;
528                                                                                 $aSearch['iSearchRank']++;
529                                                                                 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
530                                                                                 {
531                                                                                         if ($aSearch['sCountryCode'] === false)
532                                                                                         {
533                                                                                                 $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
534                                                                                                 // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
535                                                                                                 // If reverse order is enabled, it may appear at the beginning as well.
536                                                                                                 if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
537                                                                                                                 (!$bReverseInPlan || $iToken > 0 || $iPhrase > 0))
538                                                                                                 {
539                                                                                                         $aSearch['iSearchRank'] += 5;
540                                                                                                 }
541                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
542                                                                                         }
543                                                                                 }
544                                                                                 elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
545                                                                                 {
546                                                                                         if ($aSearch['fLat'] === '')
547                                                                                         {
548                                                                                                 $aSearch['fLat'] = $aSearchTerm['lat'];
549                                                                                                 $aSearch['fLon'] = $aSearchTerm['lon'];
550                                                                                                 $aSearch['fRadius'] = $aSearchTerm['radius'];
551                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
552                                                                                         }
553                                                                                 }
554                                                                                 elseif ($sPhraseType == 'postalcode')
555                                                                                 {
556                                                                                         // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
557                                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
558                                                                                         {
559                                                                                                 // If we already have a name try putting the postcode first
560                                                                                                 if (sizeof($aSearch['aName']))
561                                                                                                 {
562                                                                                                         $aNewSearch = $aSearch;
563                                                                                                         $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
564                                                                                                         $aNewSearch['aName'] = array();
565                                                                                                         $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
566                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
567                                                                                                 }
568
569                                                                                                 if (sizeof($aSearch['aName']))
570                                                                                                 {
571                                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
572                                                                                                         {
573                                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
574                                                                                                         }
575                                                                                                         else
576                                                                                                         {
577                                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
578                                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
579                                                                                                         }
580                                                                                                 }
581                                                                                                 else
582                                                                                                 {
583                                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
584                                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
585                                                                                                 }
586                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
587                                                                                         }
588
589                                                                                 }
590                                                                                 elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
591                                                                                 {
592                                                                                         if ($aSearch['sHouseNumber'] === '')
593                                                                                         {
594                                                                                                 $aSearch['sHouseNumber'] = $sToken;
595                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
596                                                                                                 /*
597                                                                                                 // Fall back to not searching for this item (better than nothing)
598                                                                                                 $aSearch = $aCurrentSearch;
599                                                                                                 $aSearch['iSearchRank'] += 1;
600                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
601                                                                                                  */
602                                                                                         }
603                                                                                 }
604                                                                                 elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
605                                                                                 {
606                                                                                         if ($aSearch['sClass'] === '')
607                                                                                         {
608                                                                                                 $aSearch['sOperator'] = $aSearchTerm['operator'];
609                                                                                                 $aSearch['sClass'] = $aSearchTerm['class'];
610                                                                                                 $aSearch['sType'] = $aSearchTerm['type'];
611                                                                                                 if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
612                                                                                                 else $aSearch['sOperator'] = 'near'; // near = in for the moment
613
614                                                                                                 // Do we have a shortcut id?
615                                                                                                 if ($aSearch['sOperator'] == 'name')
616                                                                                                 {
617                                                                                                         $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
618                                                                                                         if ($iAmenityID = $oDB->getOne($sSQL))
619                                                                                                         {
620                                                                                                                 $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
621                                                                                                                 $aSearch['aName'][$iAmenityID] = $iAmenityID;
622                                                                                                                 $aSearch['sClass'] = '';
623                                                                                                                 $aSearch['sType'] = '';
624                                                                                                         }
625                                                                                                 }
626                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
627                                                                                         }
628                                                                                 }
629                                                                                 elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
630                                                                                 {
631                                                                                         if (sizeof($aSearch['aName']))
632                                                                                         {
633                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
634                                                                                                 {
635                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
636                                                                                                 }
637                                                                                                 else
638                                                                                                 {
639                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
640                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
641                                                                                                 }
642                                                                                         }
643                                                                                         else
644                                                                                         {
645                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
646                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
647                                                                                         }
648                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
649                                                                                 }
650                                                                         }
651                                                                 }
652                                                                 if (isset($aValidTokens[$sToken]))
653                                                                 {
654                                                                         // Allow searching for a word - but at extra cost
655                                                                         foreach($aValidTokens[$sToken] as $aSearchTerm)
656                                                                         {
657                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
658                                                                                 {
659                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
660                                                                                         {
661                                                                                                 $aSearch = $aCurrentSearch;
662                                                                                                 $aSearch['iSearchRank'] += 1;
663                                                                                                 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
664                                                                                                 {
665                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
666                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
667                                                                                                 }
668                                                                                                 elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
669                                                                                                 {
670                                                                                                         foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
671                                                                                                         {
672                                                                                                                 if (empty($aSearchTermToken['country_code'])
673                                                                                                                                 && empty($aSearchTermToken['lat'])
674                                                                                                                                 && empty($aSearchTermToken['class']))
675                                                                                                                 {
676                                                                                                                         $aSearch = $aCurrentSearch;
677                                                                                                                         $aSearch['iSearchRank'] += 1;
678                                                                                                                         $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
679                                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
680                                                                                                                 }
681                                                                                                         }
682                                                                                                 }
683                                                                                                 else
684                                                                                                 {
685                                                                                                         $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
686                                                                                                         if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
687                                                                                                 }
688                                                                                         }
689
690                                                                                         if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
691                                                                                         {
692                                                                                                 $aSearch = $aCurrentSearch;
693                                                                                                 $aSearch['iSearchRank'] += 2;
694                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
695                                                                                                 if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
696                                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
697                                                                                                 else
698                                                                                                         $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
699                                                                                                 $aSearch['iNamePhrase'] = $iPhrase;
700                                                                                                 if ($aSearch['iSearchRank'] < $iMaxRank) $aNewWordsetSearches[] = $aSearch;
701                                                                                         }
702                                                                                 }
703                                                                         }
704                                                                 }
705                                                                 else
706                                                                 {
707                                                                         // Allow skipping a word - but at EXTREAM cost
708                                                                         //$aSearch = $aCurrentSearch;
709                                                                         //$aSearch['iSearchRank']+=100;
710                                                                         //$aNewWordsetSearches[] = $aSearch;
711                                                                 }
712                                                         }
713                                                         // Sort and cut
714                                                         usort($aNewWordsetSearches, 'bySearchRank');
715                                                         $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
716                                                 }
717                                                 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
718
719                                                 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
720                                                 usort($aNewPhraseSearches, 'bySearchRank');
721
722                                                 $aSearchHash = array();
723                                                 foreach($aNewPhraseSearches as $iSearch => $aSearch)
724                                                 {
725                                                         $sHash = serialize($aSearch);
726                                                         if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
727                                                         else $aSearchHash[$sHash] = 1;
728                                                 }
729
730                                                 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
731                                         }
732
733                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
734                                         $aGroupedSearches = array();
735                                         foreach($aNewPhraseSearches as $aSearch)
736                                         {
737                                                 if ($aSearch['iSearchRank'] < $iMaxRank)
738                                                 {
739                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
740                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
741                                                 }
742                                         }
743                                         ksort($aGroupedSearches);
744
745                                         $iSearchCount = 0;
746                                         $aSearches = array();
747                                         foreach($aGroupedSearches as $iScore => $aNewSearches)
748                                         {
749                                                 $iSearchCount += sizeof($aNewSearches);
750                                                 $aSearches = array_merge($aSearches, $aNewSearches);
751                                                 if ($iSearchCount > 50) break;
752                                         }
753
754                                         //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
755
756                                 }
757
758                         }
759                         else
760                         {
761                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
762                                 $aGroupedSearches = array();
763                                 foreach($aSearches as $aSearch)
764                                 {
765                                         if ($aSearch['iSearchRank'] < $iMaxRank)
766                                         {
767                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
768                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
769                                         }
770                                 }
771                                 ksort($aGroupedSearches);
772                         }
773
774                         if (CONST_Debug) var_Dump($aGroupedSearches);
775
776                         if ($bReverseInPlan)
777                         {
778                                 $aCopyGroupedSearches = $aGroupedSearches;
779                                 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
780                                 {
781                                         foreach($aSearches as $iSearch => $aSearch)
782                                         {
783                                                 if (sizeof($aSearch['aAddress']))
784                                                 {
785                                                         $iReverseItem = array_pop($aSearch['aAddress']);
786                                                         if (isset($aPossibleMainWordIDs[$iReverseItem]))
787                                                         {
788                                                                 $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
789                                                                 $aSearch['aName'] = array($iReverseItem);
790                                                                 $aGroupedSearches[$iGroup][] = $aSearch;
791                                                         }
792                                                         //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
793                                                         //$aGroupedSearches[$iGroup][] = $aReverseSearch;
794                                                 }
795                                         }
796                                 }
797                         }
798
799                         if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
800                         {
801                                 $aCopyGroupedSearches = $aGroupedSearches;
802                                 foreach($aCopyGroupedSearches as $iGroup => $aSearches)
803                                 {
804                                         foreach($aSearches as $iSearch => $aSearch)
805                                         {
806                                                 $aReductionsList = array($aSearch['aAddress']);
807                                                 $iSearchRank = $aSearch['iSearchRank'];
808                                                 while(sizeof($aReductionsList) > 0)
809                                                 {
810                                                         $iSearchRank += 5;
811                                                         if ($iSearchRank > iMaxRank) break 3;
812                                                         $aNewReductionsList = array();
813                                                         foreach($aReductionsList as $aReductionsWordList)
814                                                         {
815                                                                 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
816                                                                 {
817                                                                         $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
818                                                                         $aReverseSearch = $aSearch;
819                                                                         $aSearch['aAddress'] = $aReductionsWordListResult;
820                                                                         $aSearch['iSearchRank'] = $iSearchRank;
821                                                                         $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
822                                                                         if (sizeof($aReductionsWordListResult) > 0)
823                                                                         {
824                                                                                 $aNewReductionsList[] = $aReductionsWordListResult;
825                                                                         }
826                                                                 }
827                                                         }
828                                                         $aReductionsList = $aNewReductionsList;
829                                                 }
830                                         }
831                                 }
832                                 ksort($aGroupedSearches);
833                         }
834
835                         // Filter out duplicate searches
836                         $aSearchHash = array();
837                         foreach($aGroupedSearches as $iGroup => $aSearches)
838                         {
839                                 foreach($aSearches as $iSearch => $aSearch)
840                                 {
841                                         $sHash = serialize($aSearch);
842                                         if (isset($aSearchHash[$sHash]))
843                                         {
844                                                 unset($aGroupedSearches[$iGroup][$iSearch]);
845                                                 if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
846                                         }
847                                         else
848                                         {
849                                                 $aSearchHash[$sHash] = 1;
850                                         }
851                                 }
852                         }
853
854                         if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
855
856                         $iGroupLoop = 0;
857                         $iQueryLoop = 0;
858                         foreach($aGroupedSearches as $iGroupedRank => $aSearches)
859                         {
860                                 $iGroupLoop++;
861                                 foreach($aSearches as $aSearch)
862                                 {
863                                         $iQueryLoop++;
864
865                                         if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
866                                         if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
867
868
869                                         // Must have a location term
870                                         if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
871                                         {
872                                                 if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
873                                                 {
874                                                         if (4 >= $iMinAddressRank && 4 <= $iMaxAddressRank)
875                                                         {
876                                                                 $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
877                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
878                                                                 $sSQL .= " order by st_area(geometry) desc limit 1";
879                                                                 if (CONST_Debug) var_dump($sSQL);
880                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
881                                                         }
882                                                 }
883                                                 else
884                                                 {
885                                                         if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
886                                                         if (!$aSearch['sClass']) continue;
887                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
888                                                         if ($oDB->getOne($sSQL))
889                                                         {
890                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
891                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
892                                                                 $sSQL .= " where st_contains($sViewboxSmallSQL, ct.centroid)";
893                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
894                                                                 if (sizeof($aExcludePlaceIDs))
895                                                                 {
896                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
897                                                                 }
898                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
899                                                                 $sSQL .= " limit $iLimit";
900                                                                 if (CONST_Debug) var_dump($sSQL);
901                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
902
903                                                                 // If excluded place IDs are given, it is fair to assume that
904                                                                 // there have been results in the small box, so no further
905                                                                 // expansion in that case.
906                                                                 if (!sizeof($aPlaceIDs) && !sizeof($aExcludePlaceIDs))
907                                                                 {
908                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
909                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
910                                                                         $sSQL .= " where st_contains($sViewboxLargeSQL, ct.centroid)";
911                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
912                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
913                                                                         $sSQL .= " limit $iLimit";
914                                                                         if (CONST_Debug) var_dump($sSQL);
915                                                                         $aPlaceIDs = $oDB->getCol($sSQL);
916                                                                 }
917                                                         }
918                                                         else
919                                                         {
920                                                                 $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
921                                                                 $sSQL .= " and st_contains($sViewboxSmallSQL, geometry) and linked_place_id is null";
922                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
923                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
924                                                                 $sSQL .= " limit $iLimit";
925                                                                 if (CONST_Debug) var_dump($sSQL);
926                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
927                                                         }
928                                                 }
929                                         }
930                                         else
931                                         {
932                                                 $aPlaceIDs = array();
933
934                                                 // First we need a position, either aName or fLat or both
935                                                 $aTerms = array();
936                                                 $aOrder = array();
937
938                                                 // TODO: filter out the pointless search terms (2 letter name tokens and less)
939                                                 // they might be right - but they are just too darned expensive to run
940                                                 if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
941                                                 if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
942                                                 if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
943                                                 {
944                                                         // For infrequent name terms disable index usage for address
945                                                         if (CONST_Search_NameOnlySearchFrequencyThreshold &&
946                                                                         sizeof($aSearch['aName']) == 1 &&
947                                                                         $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
948                                                         {
949                                                                 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
950                                                         }
951                                                         else
952                                                         {
953                                                                 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
954                                                                 if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
955                                                         }
956                                                 }
957                                                 if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
958                                                 if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
959                                                 if ($aSearch['fLon'] && $aSearch['fLat'])
960                                                 {
961                                                         $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
962                                                         $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
963                                                 }
964                                                 if (sizeof($aExcludePlaceIDs))
965                                                 {
966                                                         $aTerms[] = "place_id not in (".join(',',$aExcludePlaceIDs).")";
967                                                 }
968                                                 if ($sCountryCodesSQL)
969                                                 {
970                                                         $aTerms[] = "country_code in ($sCountryCodesSQL)";
971                                                 }
972
973                                                 if ($bBoundingBoxSearch) $aTerms[] = "centroid && $sViewboxSmallSQL";
974                                                 if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
975
976                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
977                                                 if ($sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
978                                                 if ($sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
979                                                 $aOrder[] = "$sImportanceSQL DESC";
980                                                 if (sizeof($aSearch['aFullNameAddress']))
981                                                 {
982                                                         $aOrder[] = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) DESC';
983                                                 }
984
985                                                 if (sizeof($aTerms))
986                                                 {
987                                                         $sSQL = "select place_id";
988                                                         $sSQL .= " from search_name";
989                                                         $sSQL .= " where ".join(' and ',$aTerms);
990                                                         $sSQL .= " order by ".join(', ',$aOrder);
991                                                         if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
992                                                                 $sSQL .= " limit 50";
993                                                         elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
994                                                                 $sSQL .= " limit 1";
995                                                         else
996                                                                 $sSQL .= " limit ".$iLimit;
997
998                                                         if (CONST_Debug) { var_dump($sSQL); }
999                                                         $aViewBoxPlaceIDs = $oDB->getAll($sSQL);
1000                                                         if (PEAR::IsError($aViewBoxPlaceIDs))
1001                                                         {
1002                                                                 failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1003                                                         }
1004                                                         //var_dump($aViewBoxPlaceIDs);
1005                                                         // Did we have an viewbox matches?
1006                                                         $aPlaceIDs = array();
1007                                                         $bViewBoxMatch = false;
1008                                                         foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1009                                                         {
1010                                                                 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1011                                                                 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1012                                                                 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1013                                                                 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1014                                                                 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1015                                                         }
1016                                                 }
1017                                                 //var_Dump($aPlaceIDs);
1018                                                 //exit;
1019
1020                                                 if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1021                                                 {
1022                                                         $aRoadPlaceIDs = $aPlaceIDs;
1023                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1024
1025                                                         // Now they are indexed look for a house attached to a street we found
1026                                                         $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1027                                                         $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1028                                                         if (sizeof($aExcludePlaceIDs))
1029                                                         {
1030                                                                 $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
1031                                                         }
1032                                                         $sSQL .= " limit $iLimit";
1033                                                         if (CONST_Debug) var_dump($sSQL);
1034                                                         $aPlaceIDs = $oDB->getCol($sSQL);
1035
1036                                                         // If not try the aux fallback table
1037                                                         if (!sizeof($aPlaceIDs))
1038                                                         {
1039                                                                 $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1040                                                                 if (sizeof($aExcludePlaceIDs))
1041                                                                 {
1042                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
1043                                                                 }
1044                                                                 //$sSQL .= " limit $iLimit";
1045                                                                 if (CONST_Debug) var_dump($sSQL);
1046                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
1047                                                         }
1048
1049                                                         if (!sizeof($aPlaceIDs))
1050                                                         {
1051                                                                 $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1052                                                                 if (sizeof($aExcludePlaceIDs))
1053                                                                 {
1054                                                                         $sSQL .= " and place_id not in (".join(',',$aExcludePlaceIDs).")";
1055                                                                 }
1056                                                                 //$sSQL .= " limit $iLimit";
1057                                                                 if (CONST_Debug) var_dump($sSQL);
1058                                                                 $aPlaceIDs = $oDB->getCol($sSQL);
1059                                                         }
1060
1061                                                         // Fallback to the road
1062                                                         if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1063                                                         {
1064                                                                 $aPlaceIDs = $aRoadPlaceIDs;
1065                                                         }
1066
1067                                                 }
1068
1069                                                 if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1070                                                 {
1071                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1072                                                         $aClassPlaceIDs = array();
1073
1074                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1075                                                         {
1076                                                                 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1077                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1078                                                                 $sSQL .= " and linked_place_id is null";
1079                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1080                                                                 $sSQL .= " order by rank_search asc limit $iLimit";
1081                                                                 if (CONST_Debug) var_dump($sSQL);
1082                                                                 $aClassPlaceIDs = $oDB->getCol($sSQL);
1083                                                         }
1084
1085                                                         if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1086                                                         {
1087                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1088                                                                 $bCacheTable = $oDB->getOne($sSQL);
1089
1090                                                                 $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1091
1092                                                                 if (CONST_Debug) var_dump($sSQL);
1093                                                                 $iMaxRank = ((int)$oDB->getOne($sSQL));
1094
1095                                                                 // For state / country level searches the normal radius search doesn't work very well
1096                                                                 $sPlaceGeom = false;
1097                                                                 if ($iMaxRank < 9 && $bCacheTable)
1098                                                                 {
1099                                                                         // Try and get a polygon to search in instead
1100                                                                         $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1101                                                                         if (CONST_Debug) var_dump($sSQL);
1102                                                                         $sPlaceGeom = $oDB->getOne($sSQL);
1103                                                                 }
1104
1105                                                                 if ($sPlaceGeom)
1106                                                                 {
1107                                                                         $sPlaceIDs = false;
1108                                                                 }
1109                                                                 else
1110                                                                 {
1111                                                                         $iMaxRank += 5;
1112                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
1113                                                                         if (CONST_Debug) var_dump($sSQL);
1114                                                                         $aPlaceIDs = $oDB->getCol($sSQL);
1115                                                                         $sPlaceIDs = join(',',$aPlaceIDs);
1116                                                                 }
1117
1118                                                                 if ($sPlaceIDs || $sPlaceGeom)
1119                                                                 {
1120
1121                                                                         $fRange = 0.01;
1122                                                                         if ($bCacheTable)
1123                                                                         {
1124                                                                                 // More efficient - can make the range bigger
1125                                                                                 $fRange = 0.05;
1126
1127                                                                                 $sOrderBySQL = '';
1128                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1129                                                                                 else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1130                                                                                 else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1131
1132                                                                                 $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1133                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1134                                                                                 if ($sPlaceIDs)
1135                                                                                 {
1136                                                                                         $sSQL .= ",placex as f where ";
1137                                                                                         $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1138                                                                                 }
1139                                                                                 if ($sPlaceGeom)
1140                                                                                 {
1141                                                                                         $sSQL .= " where ";
1142                                                                                         $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1143                                                                                 }
1144                                                                                 if (sizeof($aExcludePlaceIDs))
1145                                                                                 {
1146                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1147                                                                                 }
1148                                                                                 if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1149                                                                                 if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1150                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1151                                                                                 $sSQL .= " limit $iLimit";
1152                                                                                 if (CONST_Debug) var_dump($sSQL);
1153                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1154                                                                         }
1155                                                                         else
1156                                                                         {
1157                                                                                 if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1158
1159                                                                                 $sOrderBySQL = '';
1160                                                                                 if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1161                                                                                 else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1162
1163                                                                                 $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1164                                                                                 $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1165                                                                                 $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1166                                                                                 if (sizeof($aExcludePlaceIDs))
1167                                                                                 {
1168                                                                                         $sSQL .= " and l.place_id not in (".join(',',$aExcludePlaceIDs).")";
1169                                                                                 }
1170                                                                                 if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1171                                                                                 if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1172                                                                                 if ($iOffset) $sSQL .= " offset $iOffset";
1173                                                                                 $sSQL .= " limit $iLimit";
1174                                                                                 if (CONST_Debug) var_dump($sSQL);
1175                                                                                 $aClassPlaceIDs = array_merge($aClassPlaceIDs, $oDB->getCol($sSQL));
1176                                                                         }
1177                                                                 }
1178                                                         }
1179
1180                                                         $aPlaceIDs = $aClassPlaceIDs;
1181
1182                                                 }
1183
1184                                         }
1185
1186                                         if (PEAR::IsError($aPlaceIDs))
1187                                         {
1188                                                 failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1189                                         }
1190
1191                                         if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1192
1193                                         foreach($aPlaceIDs as $iPlaceID)
1194                                         {
1195                                                 $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1196                                         }
1197                                         if ($iQueryLoop > 20) break;
1198                                 }
1199
1200                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($iMinAddressRank != 0 || $iMaxAddressRank != 30))
1201                                 {
1202                                         // Need to verify passes rank limits before dropping out of the loop (yuk!)
1203                                         $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1204                                         $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1205                                         if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1206                                         if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1207                                         $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1208                                         $sSQL .= "and (30 between $iMinAddressRank and $iMaxAddressRank ";
1209                                         if ($aAddressRankList) $sSQL .= " OR 30 in (".join(',',$aAddressRankList).")";
1210                                         $sSQL .= ")";
1211                                         if (CONST_Debug) var_dump($sSQL);
1212                                         $aResultPlaceIDs = $oDB->getCol($sSQL);
1213                                 }
1214
1215
1216                                 //exit;
1217                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1218                                 if ($iGroupLoop > 4) break;
1219                                 if ($iQueryLoop > 30) break;
1220                         }
1221
1222                         // Did we find anything?
1223                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1224                         {
1225                                 //var_Dump($aResultPlaceIDs);exit;
1226                                 // Get the details for display (is this a redundant extra step?)
1227                                 $sPlaceIDs = join(',',$aResultPlaceIDs);
1228                                 $sOrderSQL = 'CASE ';
1229                                 foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1230                                 {
1231                                         $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1232                                 }
1233                                 $sOrderSQL .= ' ELSE 10000000 END';
1234                                 $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id,calculated_country_code as country_code,";
1235                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1236                                 $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1237                                 $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1238                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1239                                 //$sSQL .= $sOrderSQL." as porder, ";
1240                                 $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1241                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(placex.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1242                                 $sSQL .= "(extratags->'place') as extra_place ";
1243                                 $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1244                                 $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1245                                 if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1246                                 if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1247                                 $sSQL .= ") ";
1248                                 if ($sAllowedTypesSQLList) $sSQL .= "and placex.class in $sAllowedTypesSQLList ";
1249                                 $sSQL .= "and linked_place_id is null ";
1250                                 $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1251                                 if (!$bDeDupe) $sSQL .= ",place_id";
1252                                 $sSQL .= ",langaddress ";
1253                                 $sSQL .= ",placename ";
1254                                 $sSQL .= ",ref ";
1255                                 $sSQL .= ",extratags->'place' ";
1256                                 $sSQL .= " union ";
1257                                 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1258                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1259                                 $sSQL .= "null as placename,";
1260                                 $sSQL .= "null as ref,";
1261                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1262                                 //$sSQL .= $sOrderSQL." as porder, ";
1263                                 $sSQL .= "-0.15 as importance, ";
1264                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1265                                 $sSQL .= "null as extra_place ";
1266                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1267                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1268                                 $sSQL .= "group by place_id";
1269                                 if (!$bDeDupe) $sSQL .= ",place_id";
1270                                 $sSQL .= " union ";
1271                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1272                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1273                                 $sSQL .= "null as placename,";
1274                                 $sSQL .= "null as ref,";
1275                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1276                                 //$sSQL .= $sOrderSQL." as porder, ";
1277                                 $sSQL .= "-0.10 as importance, ";
1278                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
1279                                 $sSQL .= "null as extra_place ";
1280                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1281                                 $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1282                                 $sSQL .= "group by place_id";
1283                                 if (!$bDeDupe) $sSQL .= ",place_id";
1284                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1285                                 $sSQL .= "order by importance desc";
1286                                 //$sSQL .= "order by rank_search,rank_address,porder asc";
1287                                 if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
1288                                 $aSearchResults = $oDB->getAll($sSQL);
1289                                 //var_dump($sSQL,$aSearchResults);exit;
1290
1291                                 if (PEAR::IsError($aSearchResults))
1292                                 {
1293                                         failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
1294                                 }
1295                         }
1296                 } // end if ($sQuery)
1297                 else
1298                 {
1299                         if (isset($_GET['nearlat']) && trim($_GET['nearlat'])!=='' && isset($_GET['nearlon']) && trim($_GET['nearlon']) !== '')
1300                         {
1301                                 $iPlaceID = geocodeReverse((float)$_GET['nearlat'], (float)$_GET['nearlon']);
1302
1303                                 if ($iPlaceID)
1304                                 {
1305                                         $aResultPlaceIDs = array($iPlaceID);
1306                                         // TODO: this needs refactoring!
1307
1308                                         // Get the details for display (is this a redundant extra step?)
1309                                         $sPlaceIDs = join(',',$aResultPlaceIDs);
1310                                         $sOrderSQL = 'CASE ';
1311                                         foreach(array_keys($aResultPlaceIDs) as $iOrder => $iPlaceID)
1312                                         {
1313                                                 $sOrderSQL .= 'when min(place_id) = '.$iPlaceID.' then '.$iOrder.' ';
1314                                         }
1315                                         $sOrderSQL .= ' ELSE 10000000 END';
1316                                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id,calculated_country_code as country_code,";
1317                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1318                                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
1319                                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
1320                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1321                                         //$sSQL .= $sOrderSQL." as porder, ";
1322                                         $sSQL .= "coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
1323                                         $sSQL .= "(extratags->'place') as extra_place ";
1324                                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
1325                                         $sSQL .= "and (placex.rank_address between $iMinAddressRank and $iMaxAddressRank ";
1326                                         if (14 >= $iMinAddressRank && 14 <= $iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1327                                         if ($aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$aAddressRankList).")";
1328                                         $sSQL .= ") ";
1329                                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
1330                                         if (!$bDeDupe) $sSQL .= ",place_id";
1331                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1332                                         $sSQL .= ",get_name_by_language(name, $sLanguagePrefArraySQL) ";
1333                                         $sSQL .= ",get_name_by_language(name, ARRAY['ref']) ";
1334                                         $sSQL .= ",extratags->'place' ";
1335                                         $sSQL .= " union ";
1336                                         $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1337                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1338                                         $sSQL .= "null as placename,";
1339                                         $sSQL .= "null as ref,";
1340                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1341                                         //$sSQL .= $sOrderSQL." as porder, ";
1342                                         $sSQL .= "-0.15 as importance, ";
1343                                         $sSQL .= "null as extra_place ";
1344                                         $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
1345                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1346                                         $sSQL .= "group by place_id";
1347                                         if (!$bDeDupe) $sSQL .= ",place_id";
1348                                         $sSQL .= " union ";
1349                                         $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id,'us' as country_code,";
1350                                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
1351                                         $sSQL .= "null as placename,";
1352                                         $sSQL .= "null as ref,";
1353                                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
1354                                         //$sSQL .= $sOrderSQL." as porder, ";
1355                                         $sSQL .= "-0.10 as importance, ";
1356                                         $sSQL .= "null as extra_place ";
1357                                         $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
1358                                         $sSQL .= "and 30 between $iMinAddressRank and $iMaxAddressRank ";
1359                                         $sSQL .= "group by place_id";
1360                                         if (!$bDeDupe) $sSQL .= ",place_id";
1361                                         $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
1362                                         $sSQL .= "order by importance desc";
1363                                         //$sSQL .= "order by rank_search,rank_address,porder asc";
1364                                         if (CONST_Debug) { echo "<hr>", var_dump($sSQL); }
1365                                         $aSearchResults = $oDB->getAll($sSQL);
1366                                         //var_dump($sSQL,$aSearchResults);exit;
1367
1368                                         if (PEAR::IsError($aSearchResults))
1369                                         {
1370                                                 failInternalError("Could not get details for place (near).", $sSQL, $aSearchResults);
1371                                         }
1372                                 }
1373                                 else
1374                                 {
1375                                         $aSearchResults = array();
1376                                 }
1377                         }
1378                 }
1379         }
1380
1381         $sSearchResult = '';
1382         if (!sizeof($aSearchResults) && isset($_GET['q']) && $_GET['q'])
1383         {
1384                 $sSearchResult = 'No Results Found';
1385         }
1386         //var_Dump($aSearchResults);
1387         //exit;
1388         $aClassType = getClassTypesWithImportance();
1389         $aRecheckWords = preg_split('/\b/u',$sQuery);
1390         foreach($aRecheckWords as $i => $sWord)
1391         {
1392                 if (!$sWord) unset($aRecheckWords[$i]);
1393         }
1394         foreach($aSearchResults as $iResNum => $aResult)
1395         {
1396                 if (CONST_Search_AreaPolygons)
1397                 {
1398                         // Get the bounding box and outline polygon
1399                         $sSQL = "select place_id,numfeatures,area,outline,";
1400                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(outline)),2)) as maxlat,";
1401                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(outline)),3)) as maxlon,";
1402                         $sSQL .= "ST_AsText(outline) as outlinestring from get_place_boundingbox_quick(".$aResult['place_id'].")";
1403
1404                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1405                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1406                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1407                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1408                         if ($bAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1409                         if ($bAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1410                         if ($bAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1411                         if ($bAsText || $bShowPolygons) $sSQL .= ",ST_AsText(geometry) as astext";
1412                         $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1413                         $aPointPolygon = $oDB->getRow($sSQL);
1414                         if (PEAR::IsError($aPointPolygon))
1415                         {
1416                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1417                         }
1418                         if ($aPointPolygon['place_id'])
1419                         {
1420                                 if ($bAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1421                                 if ($bAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1422                                 if ($bAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1423                                 if ($bAsText) $aResult['astext'] = $aPointPolygon['astext'];
1424
1425                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1426                                 {
1427                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1428                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1429                                 }
1430                                 if ($bShowPolygons)
1431                                 {
1432                                         // Translate geometary string to point array
1433                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1434                                         {
1435                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1436                                         }
1437                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1438                                         {
1439                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1440                                         }
1441                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1442                                         {
1443                                                 $fRadius = 0.01;
1444                                                 $iSteps = ($fRadius * 40000)^2;
1445                                                 $fStepSize = (2*pi())/$iSteps;
1446                                                 $aPolyPoints = array();
1447                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1448                                                 {
1449                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1450                                                 }
1451                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1452                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1453                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1454                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1455                                         }
1456                                 }
1457
1458                                 // Output data suitable for display (points and a bounding box)
1459                                 if ($bShowPolygons && isset($aPolyPoints))
1460                                 {
1461                                         $aResult['aPolyPoints'] = array();
1462                                         foreach($aPolyPoints as $aPoint)
1463                                         {
1464                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1465                                         }
1466                                 }
1467                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1468                         }
1469                 }
1470
1471                 if ($aResult['extra_place'] == 'city')
1472                 {
1473                         $aResult['class'] = 'place';
1474                         $aResult['type'] = 'city';
1475                         $aResult['rank_search'] = 16;
1476                 }
1477
1478                 if (!isset($aResult['aBoundingBox']))
1479                 {
1480                         // Default
1481                         $fDiameter = 0.0001;
1482
1483                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1484                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1485                         {
1486                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1487                         }
1488                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1489                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1490                         {
1491                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1492                         }
1493                         $fRadius = $fDiameter / 2;
1494
1495                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1496                         $fStepSize = (2*pi())/$iSteps;
1497                         $aPolyPoints = array();
1498                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1499                         {
1500                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1501                         }
1502                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1503                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1504                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1505                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1506
1507                         // Output data suitable for display (points and a bounding box)
1508                         if ($bShowPolygons)
1509                         {
1510                                 $aResult['aPolyPoints'] = array();
1511                                 foreach($aPolyPoints as $aPoint)
1512                                 {
1513                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1514                                 }
1515                         }
1516                         $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1517                 }
1518
1519                 // Is there an icon set for this type of result?
1520                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1521                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1522                 {
1523                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1524                 }
1525
1526                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1527                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1528                 {
1529                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1530                 }
1531
1532                 if ($bShowAddressDetails)
1533                 {
1534                         $aResult['address'] = getAddressDetails($oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1535                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1536                         {
1537                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1538                         }
1539
1540                         //var_dump($aResult['address']);
1541                         //exit;
1542                 }
1543
1544                 // Adjust importance for the number of exact string matches in the result
1545                 $aResult['importance'] = max(0.001,$aResult['importance']);
1546                 $iCountWords = 0;
1547                 $sAddress = $aResult['langaddress'];
1548                 foreach($aRecheckWords as $i => $sWord)
1549                 {
1550                         if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1551                 }
1552
1553                 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1554
1555                 //if (CONST_Debug) var_dump($aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']);
1556                 /*
1557                    if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1558                    && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'])
1559                    {
1560                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['importance'];
1561                    }
1562                    elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1563                    && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1564                    {
1565                    $aResult['importance'] = $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1566                    }
1567                    else
1568                    {
1569                    $aResult['importance'] = 1000000000000000;
1570                    }
1571                  */
1572                 $aResult['name'] = $aResult['langaddress'];
1573                 $aResult['foundorder'] = -$aResult['addressimportance'];
1574                 $aSearchResults[$iResNum] = $aResult;
1575         }
1576         uasort($aSearchResults, 'byImportance');
1577
1578         $aOSMIDDone = array();
1579         $aClassTypeNameDone = array();
1580         $aToFilter = $aSearchResults;
1581         $aSearchResults = array();
1582
1583         $bFirst = true;
1584         foreach($aToFilter as $iResNum => $aResult)
1585         {
1586                 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1587                 $aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1588                 if ($bFirst)
1589                 {
1590                         $fLat = $aResult['lat'];
1591                         $fLon = $aResult['lon'];
1592                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1593                         $bFirst = false;
1594                 }
1595                 if (!$bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1596                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1597                 {
1598                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1599                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1600                         $aSearchResults[] = $aResult;
1601                 }
1602
1603                 // Absolute limit on number of results
1604                 if (sizeof($aSearchResults) >= $iFinalLimit) break;
1605         }
1606
1607         $sDataDate = $oDB->getOne("select TO_CHAR(lastimportdate - '2 minutes'::interval,'YYYY/MM/DD HH24:MI')||' GMT' from import_status limit 1");
1608
1609         if (isset($_GET['nearlat']) && isset($_GET['nearlon']))
1610         {
1611                 $sQuery .= ' ['.$_GET['nearlat'].','.$_GET['nearlon'].']';
1612         }
1613
1614         if ($sQuery)
1615         {
1616                 logEnd($oDB, $hLog, sizeof($aToFilter));
1617         }
1618         $sMoreURL = CONST_Website_BaseURL.'search?format='.urlencode($sOutputFormat).'&exclude_place_ids='.join(',',$aExcludePlaceIDs);
1619         if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) $sMoreURL .= '&accept-language='.$_SERVER["HTTP_ACCEPT_LANGUAGE"];
1620         if ($bShowPolygons) $sMoreURL .= '&polygon=1';
1621         if ($bShowAddressDetails) $sMoreURL .= '&addressdetails=1';
1622         if (isset($_GET['viewbox']) && $_GET['viewbox']) $sMoreURL .= '&viewbox='.urlencode($_GET['viewbox']);
1623         if (isset($_GET['nearlat']) && isset($_GET['nearlon'])) $sMoreURL .= '&nearlat='.(float)$_GET['nearlat'].'&nearlon='.(float)$_GET['nearlon'];
1624         $sMoreURL .= '&q='.urlencode($sQuery);
1625
1626         if (CONST_Debug) exit;
1627
1628         include(CONST_BasePath.'/lib/template/search-'.$sOutputFormat.'.php');