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