Index: /trunk/include/functions_search.inc.php
===================================================================
--- /trunk/include/functions_search.inc.php	(revision 28064)
+++ /trunk/include/functions_search.inc.php	(revision 28065)
@@ -297,4 +297,78 @@
 define('QST_WILDCARD', QST_WILDCARD_BEGIN|QST_WILDCARD_END);
 
+
+class QSearchScope
+{
+  var $id;
+  var $aliases;
+  var $is_text;
+  var $allow_empty;
+
+  function __construct($id, $aliases, $allow_empty=false, $is_text=true)
+  {
+    $this->id = $id;
+    $this->aliases = $aliases;
+    $this->is_text = $is_text;
+    $this->allow_empty =$allow_empty;
+  }
+}
+
+class QNumericRangeScope extends QSearchScope
+{
+  function __construct($id, $aliases, $allow_empty=false)
+  {
+    parent::__construct($id, $aliases, $allow_empty, false);
+  }
+
+  function parse($token)
+  {
+    $str = $token->term;
+    if ( ($pos = strpos($str, '..')) !== false)
+      $range = array( substr($str,0,$pos), substr($str, $pos+2));
+    else
+      $range = array($str, $str);
+    foreach ($range as $i =>&$val)
+    {
+      if (preg_match('/^([0-9.]+)([km])?/i', $val, $matches))
+      {
+        $val = floatval($matches[1]);
+        if (isset($matches[2]))
+        {
+          if ($matches[2]=='k' || $matches[2]=='K')
+          {
+            $val *= 1000;
+            if ($i) $val += 999;
+          }
+          if ($matches[2]=='m' || $matches[2]=='M')
+          {
+            $val *= 1000000;
+            if ($i) $val += 999999;
+          }
+        }
+      }
+      else
+        $val = '';
+    }
+
+    if (!$this->allow_empty && $range[0]=='' && $range[1] == '')
+      return false;
+    $token->scope_data = $range;
+    return true;
+  }
+
+  function get_sql($field, $token)
+  {
+    $clauses = array();
+    if ($token->scope_data[0]!=='')
+      $clauses[] = $field.' >= ' .$token->scope_data[0].' ';
+    if ($token->scope_data[1]!=='')
+      $clauses[] = $field.' <= ' .$token->scope_data[1].' ';
+
+    if (empty($clauses))
+      return $field.' IS NULL';
+    return '('.implode(' AND ', $clauses).')';
+  }
+}
+
 /**
  * Analyzes and splits the quick/query search query $q into tokens.
@@ -310,15 +384,33 @@
 {
   var $is_single = true;
+  var $modifier;
   var $term; /* the actual word/phrase string*/
+  var $scope;
+
+  var $scope_data;
   var $idx;
 
-  function __construct($term)
+  function __construct($term, $modifier, $scope)
   {
     $this->term = $term;
-  }
-  
+    $this->modifier = $modifier;
+    $this->scope = $scope;
+  }
+
   function __toString()
   {
-    return $this->term;
+    $s = '';
+    if (isset($this->scope))
+      $s .= $this->scope->id .':';
+    if ($this->modifier & QST_WILDCARD_BEGIN)
+      $s .= '*';
+    if ($this->modifier & QST_QUOTED)
+      $s .= '"';
+    $s .= $this->term;
+    if ($this->modifier & QST_QUOTED)
+      $s .= '"';
+    if ($this->modifier & QST_WILDCARD_END)
+      $s .= '*';
+    return $s;
   }
 }
@@ -328,6 +420,6 @@
 {
   var $is_single = false;
+  var $modifier;
   var $tokens = array(); // the actual array of QSingleToken or QMultiToken
-  var $token_modifiers = array(); // modifiers (OR,NOT,...) for every token
 
   function __toString()
@@ -336,5 +428,5 @@
     for ($i=0; $i<count($this->tokens); $i++)
     {
-      $modifier = $this->token_modifiers[$i];
+      $modifier = $this->tokens[$i]->modifier;
       if ($i)
         $s .= ' ';
@@ -343,8 +435,4 @@
       if ($modifier & QST_NOT)
         $s .= 'NOT ';
-      if ($modifier & QST_WILDCARD_BEGIN)
-        $s .= '*';
-      if ($modifier & QST_QUOTED)
-        $s .= '"';
       if (! ($this->tokens[$i]->is_single) )
       {
@@ -357,19 +445,17 @@
         $s .= $this->tokens[$i];
       }
-      if ($modifier & QST_QUOTED)
-        $s .= '"';
-      if ($modifier & QST_WILDCARD_END)
-        $s .= '*';
-
     }
     return $s;
   }
 
-  private function push(&$token, &$modifier)
-  {
-    $this->tokens[] = new QSingleToken($token);
-    $this->token_modifiers[] = $modifier;
+  private function push(&$token, &$modifier, &$scope)
+  {
+    if (strlen($token) || (isset($scope) && $scope->allow_empty))
+    {
+      $this->tokens[] = new QSingleToken($token, $modifier, $scope);
+    }
     $token = "";
     $modifier = 0;
+    $scope = null;
   }
 
@@ -381,8 +467,9 @@
   * @param int $level the depth from root in the tree (number of opened and unclosed opening brackets)
   */
-  protected function parse_expression($q, &$qi, $level)
+  protected function parse_expression($q, &$qi, $level, $root)
   {
     $crt_token = "";
     $crt_modifier = 0;
+    $crt_scope = null;
 
     for ($stop=false; !$stop && $qi<strlen($q); $qi++)
@@ -395,11 +482,16 @@
           case '(':
             if (strlen($crt_token))
-              $this->push($crt_token, $crt_modifier);
+              $this->push($crt_token, $crt_modifier, $crt_scope);
             $sub = new QMultiToken;
             $qi++;
-            $sub->parse_expression($q, $qi, $level+1);
+            $sub->parse_expression($q, $qi, $level+1, $root);
+            $sub->modifier = $crt_modifier;
+            if (isset($crt_scope) && $crt_scope->is_text)
+            {
+              $sub->apply_scope($crt_scope); // eg. 'tag:(John OR Bill)'
+            }
             $this->tokens[] = $sub;
-            $this->token_modifiers[] = $crt_modifier;
             $crt_modifier = 0;
+            $crt_scope = null;
             break;
           case ')':
@@ -407,11 +499,23 @@
               $stop = true;
             break;
+          case ':':
+            $scope = @$root->scopes[$crt_token];
+            if (!isset($scope) || isset($crt_scope))
+            { // white space
+              $this->push($crt_token, $crt_modifier, $crt_scope);
+            }
+            else
+            {
+              $crt_token = "";
+              $crt_scope = $scope;
+            }
+            break;
           case '"':
             if (strlen($crt_token))
-              $this->push($crt_token, $crt_modifier);
+              $this->push($crt_token, $crt_modifier, $crt_scope);
             $crt_modifier |= QST_QUOTED;
             break;
           case '-':
-            if (strlen($crt_token))
+            if (strlen($crt_token) || isset($crt_scope))
               $crt_token .= $ch;
             else
@@ -424,9 +528,16 @@
               $crt_modifier |= QST_WILDCARD_BEGIN;
             break;
+          case '.':
+            if (isset($crt_scope) && !$crt_scope->is_text)
+            {
+              $crt_token .= $ch;
+              break;
+            }
+            // else white space go on..
           default:
             if (preg_match('/[\s,.;!\?]+/', $ch))
             { // white space
               if (strlen($crt_token))
-                $this->push($crt_token, $crt_modifier);
+                $this->push($crt_token, $crt_modifier, $crt_scope);
               $crt_modifier = 0;
             }
@@ -445,5 +556,5 @@
             $qi++;
           }
-          $this->push($crt_token, $crt_modifier);
+          $this->push($crt_token, $crt_modifier, $crt_scope);
         }
         else
@@ -452,6 +563,5 @@
     }
 
-    if (strlen($crt_token))
-      $this->push($crt_token, $crt_modifier);
+    $this->push($crt_token, $crt_modifier, $crt_scope);
 
     for ($i=0; $i<count($this->tokens); $i++)
@@ -461,30 +571,38 @@
       if ($token->is_single)
       {
-        if ( ($this->token_modifiers[$i]&QST_QUOTED)==0 )
-        {
-          if ('not' == strtolower($token->term))
+        if (!isset($token->scope))
+        {
+          if ( ($token->modifier & QST_QUOTED)==0 )
           {
-            if ($i+1 < count($this->tokens))
-              $this->token_modifiers[$i+1] |= QST_NOT;
-            $token->term = "";
+            if ('not' == strtolower($token->term))
+            {
+              if ($i+1 < count($this->tokens))
+                $this->tokens[$i+1]->modifier |= QST_NOT;
+              $token->term = "";
+            }
+            if ('or' == strtolower($token->term))
+            {
+              if ($i+1 < count($this->tokens))
+                $this->token[$i+1]->modifier |= QST_OR;
+              $token->term = "";
+            }
+            if ('and' == strtolower($token->term))
+            {
+              $token->term = "";
+            }
+            if ( substr($token->term, -1)=='*' )
+            {
+              $token->term = rtrim($token->term, '*');
+              $token->modifier |= QST_WILDCARD_END;
+            }
           }
-          if ('or' == strtolower($token->term))
-          {
-            if ($i+1 < count($this->tokens))
-              $this->token_modifiers[$i+1] |= QST_OR;
-            $token->term = "";
-          }
-          if ('and' == strtolower($token->term))
-          {
-            $token->term = "";
-          }
-          if ( substr($token->term, -1)=='*' )
-          {
-            $token->term = rtrim($token->term, '*');
-            $this->token_modifiers[$i] |= QST_WILDCARD_END;
-          }
-        }
-        if (!strlen($token->term))
-          $remove = true;
+          if (!strlen($token->term))
+            $remove = true;
+        }
+        elseif (!$token->scope->is_text)
+        {
+          if (!$token->scope->parse($token))
+            $remove = true;
+        }
       }
       else
@@ -496,5 +614,4 @@
       {
         array_splice($this->tokens, $i, 1);
-        array_splice($this->token_modifiers, $i, 1);
         $i--;
       }
@@ -515,8 +632,8 @@
         $this->tokens[$i]->check_operator_priority();
       if ($i==1)
-        $crt_prio = self::priority($this->token_modifiers[$i]);
+        $crt_prio = self::priority($this->tokens[$i]->modifier);
       if ($i<=1)
         continue;
-      $prio = self::priority($this->token_modifiers[$i]);
+      $prio = self::priority($this->tokens[$i]->modifier);
       if ($prio > $crt_prio)
       {// e.g. 'a OR b c d' i=2, operator(c)=AND -> prio(AND) > prio(OR) = operator(b)
@@ -524,5 +641,5 @@
         for ($j=$i+1; $j<count($this->tokens); $j++)
         {
-          if (self::priority($this->token_modifiers[$j]) >= $prio)
+          if (self::priority($this->tokens[$j]->modifier) >= $prio)
             $term_count++; // also take d
           else
@@ -534,10 +651,9 @@
         $sub = new QMultiToken;
         $sub->tokens = array_splice($this->tokens, $i, $term_count);
-        $sub->token_modifiers = array_splice($this->token_modifiers, $i, $term_count);
 
         // rewrite ourseleves as a (b c d)
         array_splice($this->tokens, $i, 0, array($sub));
-        array_splice($this->token_modifiers, $i, 0, array($sub->token_modifiers[0]&QST_OR));
-        $sub->token_modifiers[0] &= ~QST_OR;
+        $sub->modifier = $sub->tokens[0]->modifier & QST_OR;
+        $sub->tokens[0]->modifier &= ~QST_OR;
 
         $sub->check_operator_priority();
@@ -551,11 +667,18 @@
 class QExpression extends QMultiToken
 {
+  var $scopes = array();
   var $stokens = array();
   var $stoken_modifiers = array();
 
-  function __construct($q)
-  {
+  function __construct($q, $scopes)
+  {
+    foreach ($scopes as $scope)
+    {
+      $this->scopes[$scope->id] = $scope;
+      foreach ($scope->aliases as $alias)
+        $this->scopes[strtolower($alias)] = $scope;
+    }
     $i = 0;
-    $this->parse_expression($q, $i, 0);
+    $this->parse_expression($q, $i, 0, $this);
     //manipulate the tree so that 'a OR b c' is the same as 'b c OR a'
     $this->check_operator_priority();
@@ -568,5 +691,5 @@
     {
       $token = $expr->tokens[$i];
-      $crt_is_not = ($expr->token_modifiers[$i] ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation;
+      $crt_is_not = ($token->modifier ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation;
 
       if ($token->is_single)
@@ -575,5 +698,5 @@
         $this->stokens[] = $token;
 
-        $modifier = $expr->token_modifiers[$i];
+        $modifier = $token->modifier;
         if ($crt_is_not)
           $modifier |= QST_NOT;
@@ -604,5 +727,4 @@
 function qsearch_get_images(QExpression $expr, QResults $qsr)
 {
-  //@TODO: inflections for english / french
   $qsr->images_iids = array_fill(0, count($expr->tokens), array());
 
@@ -619,49 +741,78 @@
   for ($i=0; $i<count($expr->stokens); $i++)
   {
-    $token = $expr->stokens[$i]->term;
+    $token = $expr->stokens[$i];
+    $term = $token->term;
+    $scope_id = isset($token->scope) ? $token->scope->id : 'photo';
     $clauses = array();
 
-    $like = addslashes($token);
+    $like = addslashes($term);
     $like = str_replace( array('%','_'), array('\\%','\\_'), $like); // escape LIKE specials %_
-    $clauses[] = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\'';
-
-    if ($inflector!=null && strlen($token)>2
-      && ($expr->stoken_modifiers[$i] & (QST_QUOTED|QST_WILDCARD))==0
-      && strcspn($token, '\'0123456789') == strlen($token)
-      )
-    {
-      $variants = array_unique( array_diff( $inflector->get_variants($token), array($token) ) );
-      $qsr->variants[$token] = $variants;
-    }
-    else
-    {
-      $variants = array();
-    }
-
-    if (strlen($token)>3) // default minimum full text index
-    {
-      $ft = $token;
-      if ($expr->stoken_modifiers[$i] & QST_QUOTED)
-        $ft = '"'.$ft.'"';
-      if ($expr->stoken_modifiers[$i] & QST_WILDCARD_END)
-        $ft .= '*';
-      foreach ($variants as $variant)
-      {
-        $ft.=' '.$variant;
-      }
-      $clauses[] = 'MATCH(i.name, i.comment) AGAINST( \''.addslashes($ft).'\' IN BOOLEAN MODE)';
-    }
-    else
-    {
-      foreach( array('i.name', 'i.comment') as $field)
-      {
-        /*$clauses[] = $field.' LIKE \''.$like.' %\'';
-        $clauses[] = $field.' LIKE \'% '.$like.'\'';
-        $clauses[] = $field.' LIKE \'% '.$like.' %\'';*/
-        $clauses[] = $field.' REGEXP \'[[:<:]]'.addslashes(preg_quote($token)).'[[:>:]]\'';
-      }
-    }
-    $query = $query_base.'('.implode(' OR ', $clauses).')';
-    $qsr->images_iids[$i] = query2array($query,null,'id');
+    $file_like = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\'';
+
+    switch ($scope_id)
+    {
+      case 'photo':
+        $clauses[] = $file_like;
+
+        if ($inflector!=null && strlen($term)>2
+          && ($expr->stoken_modifiers[$i] & (QST_QUOTED|QST_WILDCARD))==0
+          && strcspn($term, '\'0123456789') == strlen($term)
+          )
+        {
+          $variants = array_unique( array_diff( $inflector->get_variants($term), array($term) ) );
+          $qsr->variants[$term] = $variants;
+        }
+        else
+        {
+          $variants = array();
+        }
+
+        if (strlen($term)>3) // default minimum full text index
+        {
+          $ft = $term;
+          if ($expr->stoken_modifiers[$i] & QST_QUOTED)
+            $ft = '"'.$ft.'"';
+          if ($expr->stoken_modifiers[$i] & QST_WILDCARD_END)
+            $ft .= '*';
+          foreach ($variants as $variant)
+          {
+            $ft.=' '.$variant;
+          }
+          $clauses[] = 'MATCH(i.name, i.comment) AGAINST( \''.addslashes($ft).'\' IN BOOLEAN MODE)';
+        }
+        else
+        {
+          foreach( array('i.name', 'i.comment') as $field)
+          {
+            $clauses[] = $field.' REGEXP \'[[:<:]]'.addslashes(preg_quote($term)).'[[:>:]]\'';
+          }
+        }
+        break;
+
+      case 'file':
+        $clauses[] = $file_like;
+        break;
+      case 'width':
+      case 'height':
+      case 'hits':
+      case 'rating_score':
+        $clauses[] = $token->scope->get_sql($scope_id, $token);
+        break;
+      case 'ratio':
+        $clauses[] = $token->scope->get_sql('width/height', $token);
+        break;
+      case 'size':
+        $clauses[] = $token->scope->get_sql('width*height', $token);
+        break;
+      case 'filesize':
+        $clauses[] = $token->scope->get_sql('filesize', $token);
+        break;
+
+    }
+    if (!empty($clauses))
+    {
+      $query = $query_base.'('.implode(' OR ', $clauses).')';
+      $qsr->images_iids[$i] = query2array($query,null,'id');
+    }
   }
 }
@@ -679,5 +830,12 @@
   foreach ($tokens as $token)
   {
-    $transliterated_tokens[] = transliterate($token->term);
+    if (!isset($token->scope) || 'tag' == $token->scope)
+    {
+      $transliterated_tokens[] = transliterate($token->term);
+    }
+    else
+    {
+      $transliterated_tokens[] = '';
+    }
   }
 
@@ -696,4 +854,6 @@
     {
       $transliterated_token = $transliterated_tokens[$i];
+      if (strlen($transliterated_token)==0)
+        continue;
 
       $match = false;
@@ -831,4 +991,15 @@
       $qsr->tag_iids[$i] = query2array($query, null, 'image_id');
     }
+    elseif (isset($tokens[$i]->scope) && 'tag' == $tokens[$i]->scope->id && strlen($token->term)==0)
+    {
+      if ($tokens[$i]->modifier & QST_WILDCARD)
+      {// eg. 'tag:*' returns all tagged images
+        $qsr->tag_iids[$i] = query2array('SELECT DISTINCT image_id FROM '.IMAGE_TAG_TABLE, null, 'image_id');
+      }
+      else
+      {// eg. 'tag:' returns all untagged images
+        $qsr->tag_iids[$i] = query2array('SELECT id FROM '.IMAGES_TABLE.' LEFT JOIN '.IMAGE_TAG_TABLE.' ON id=image_id WHERE image_id IS NULL', null, 'id');
+      }
+    }
   }
 }
@@ -854,5 +1025,5 @@
       $crt_ids = qsearch_eval($crt, $qsr, $crt_qualifies, $crt_ignored_terms);
 
-    $modifier = $expr->token_modifiers[$i];
+    $modifier = $crt->modifier;
     if ($modifier & QST_NOT)
       $not_ids = array_unique( array_merge($not_ids, $crt_ids));
@@ -911,5 +1082,16 @@
     );
 
-  $expression = new QExpression($q);
+  $scopes = array();
+  $scopes[] = new QSearchScope('tag', array('tags'));
+  $scopes[] = new QSearchScope('photo', array('photos'));
+  $scopes[] = new QSearchScope('file', array('filename'));
+  $scopes[] = new QNumericRangeScope('width', array());
+  $scopes[] = new QNumericRangeScope('height', array());
+  $scopes[] = new QNumericRangeScope('ratio', array());
+  $scopes[] = new QNumericRangeScope('size', array());
+  $scopes[] = new QNumericRangeScope('filesize', array());
+  $scopes[] = new QNumericRangeScope('hits', array('hit', 'visit', 'visits'));
+  $scopes[] = new QNumericRangeScope('rating_score', array('score'), true);
+  $expression = new QExpression($q, $scopes);
 //var_export($expression);
 
