--- taxonomy.module.orig	2005-05-26 12:25:04.000000000 +0200
+++ taxonomy.module	2005-05-26 23:24:01.000000000 +0200
@@ -939,6 +939,54 @@ function _taxonomy_prepare_insert($data,
   return "($result)";
 }
 
+// Creates more convenient structure from initial list of tids grouped in AND groups
+function _taxonomy_prepare_tids($tids, $depth) {
+  $t = array(
+    "max" => 0, // max number of tids in an AND group
+    "descendants" => array(), 
+    "OR" => array(),
+    "AND" => array()
+  );
+  
+  if ($depth === 'all') 
+    $depth = NULL;
+  
+  foreach ($tids as $index => $and_tids) {    
+    $tids_nr = count($and_tids);
+    if ($tids_nr > $t["max"])
+      $t["max"] = $tids_nr;
+  
+    foreach ($and_tids as $i => $tid) {
+      $bare_tid = ltrim($tid, '!'); 
+      $tid_nin = ($bare_tid == $tid ? "IN" : "NOT IN"); // nin == in/not in
+      
+      if (! isset($t["descendants"][$bare_tid])) {
+        // Get descendants
+        $term = taxonomy_get_term($bare_tid);
+        $tree = taxonomy_get_tree($term->vid, $bare_tid, -1, $depth);
+        $t["descendants"][$bare_tid] = array_merge(array($bare_tid), array_map('_taxonomy_get_tid_from_term', $tree));
+      }
+      
+      if ($tids_nr == 1) {
+        // Only 1 tid in AND group - let's put it (with it's descendants) directly into OR tids
+        $t["OR"][$tid_nin] = array_merge($t["OR"][$tid_nin], $t["descendants"][$bare_tid]);
+        break; // next $and_group
+      }
+      
+      // More then 1 tid in the AND group - add the tid to this group's IN/NOT INT group (but only it, without descendants)
+      $t["AND"][$index][$tid_nin] = array_merge($t["AND"][$index][$tid_nin], array($bare_tid));
+    }
+  }
+  
+  return $t;
+}
+
+// $op = "IN" or "NOT IN"
+function _taxonomy_make_sql($tids, $index, $op) {
+  return "tn{$index}.tid $op (". implode(',', $tids) .')';
+}
+
+
 /**
  * Finds all nodes that match selected taxonomy conditions.
  *
@@ -957,47 +1005,59 @@ function _taxonomy_prepare_insert($data,
  * @return
  *   A resource identifier pointing to the query results.
  */
-function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
-  if (count($tids) > 0) {
-    // For each term ID, generate an array of descendant term IDs to the right depth.
-    $descendant_tids = array();
-    if ($depth === 'all') {
-      $depth = NULL;
-    }
-    foreach ($tids as $index => $tid) {
-      $term = taxonomy_get_term($tid);
-      $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
-      $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree));
-    }
-
-    if ($operator == 'or') {
-      $str_tids = implode(',', call_user_func_array('array_merge', $descendant_tids));
-      $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1 ORDER BY '. $order;
-      $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1';
-    }
-    else {
-      $joins = '';
-      $wheres = '';
-      foreach ($descendant_tids as $index => $tids) {
-        $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.nid = tn'. $index .'.nid';
-        $wheres .= ' AND tn'. $index .'.tid IN ('. implode(',', $tids) .')';
-      }
-      $sql = 'SELECT n.nid, n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order;
-      $sql_count = 'SELECT COUNT(n.nid) FROM {node} n '. $joins .' WHERE n.status = 1 ' . $wheres;
-    }
-    $sql = db_rewrite_sql($sql);
-    $sql_count = db_rewrite_sql($sql_count);
-    if ($pager) {
-      $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count);
-    }
-    else {
-      $result = db_query_range($sql, 0, 15);
-    }
+function taxonomy_select_nodes($tids = array(), $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
+  if (count($tids) <= 0) {
+    return NULL;
+  }
+
+  $tids = _taxonomy_prepare_tids($tids, $depth);
+  $max_and_tids = $tids["max"];
+  $or_tids      = $tids["OR"];
+  $and_tids     = $tids["AND"];
+  $descendants  = $tids["descendants"];
+    
+  $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n';
+  $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n';
+  for ($i = 0; $i < $max_and_tids; $i++) {
+    $sql_body .= ' INNER JOIN {term_node} tn'. $i .' ON n.nid = tn'. $i .'.nid';
+  }     
+  $sql_body .= ' WHERE n.status = 1 AND (0=1';    
+  if ($or_tids["IN"])     $sql_body .= ' OR '. _taxonomy_make_sql($or_tids["IN"], 0, "IN");
+  if ($or_tids["NOT IN"]) $sql_body .= ' OR '. _taxonomy_make_sql($or_tids["NOT IN"], 0, "NOT IN");
+
+  // This will construct sql for AND groups, e.g. if we specified 1,2+3,4,!5 this should produce something like
+  // OR (1=1 AND tn0.tid IN (1) AND tn1.tid IN (2)) OR (1=1 AND tn0.tid IN (3) AND tn1.tid IN (4) AND tn2.tid NOT IN (5))
+  foreach ($and_tids as $i => $and_group) { 
+    $sql_group = ''; $index = 0;
+    if (isset($and_group["IN"])) {
+      foreach ($and_group["IN"] as $j => $tid) {
+        $sql_group .= ' AND '. _taxonomy_make_sql($descendants[$tid], $index++, "IN");
+      }
+    }
+    if (isset($and_group["NOT IN"])) {
+      foreach ($and_group["NOT IN"] as $j => $tid) {
+        $sql_group .= ' AND '. _taxonomy_make_sql($descendants[$tid], $index++, "NOT IN");
+      }
+    }
+    $sql_body .= ' OR (1=1'. $sql_group .')';      
+  } 
+    
+  $sql .= $sql_body .') ORDER BY '. $order;
+  $sql_count .= $sql_body .')';
+
+  $sql = db_rewrite_sql($sql);
+  $sql_count = db_rewrite_sql($sql_count);
+  if ($pager) {
+    $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count);
+  }
+  else {
+    $result = db_query_range($sql, 0, 15);
   }
 
   return $result;
 }
 
+
 /**
  * Accepts the result of a pager_query() call, such as that performed by
  * taxonomy_select_nodes(), and formats each node along with a pager.
@@ -1042,70 +1102,78 @@ function taxonomy_nodeapi($node, $op, $a
  * Menu callback; displays all nodes associated with a term.
  */
 function taxonomy_term_page($str_tids = '', $depth = 0, $op = 'page') {
-  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
-    $operator = 'or';
-    // The '+' character in a query string may be parsed as ' '.
-    $tids = preg_split('/[+ ]/', $str_tids);
-  }
-  else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
-    $operator = 'and';
-    $tids = explode(',', $str_tids);
-  }
-  else {
+  if(! preg_match('/^(!?[0-9]+[,+ ])*!?[0-9]+$/', $str_tids)) {
     drupal_not_found();
+    return;
   }
-
+  
   // Needed for '+' to show up in RSS discovery URLs
   $rss_tids = urlencode($str_tids);
 
-  if ($tids) {
-    $result = db_query(db_rewrite_sql('SELECT t.tid, t.name FROM {term_data} t WHERE t.tid IN (%s)', 't', 'tid'), implode(',', $tids));
-    $tids = array(); // we rebuild the $tids-array so it only contains terms the user has access to.
-    $names = array();
-    while ($term = db_fetch_object($result)) {
-      $tids[] = $term->tid;
-      $names[] = $term->name;
-    }
+  // Split first by OR (least important), then AND and then NOT
+  // E.g. for 1,2+!3 this will create 
+  // array( array ("1", "2"), 
+  //        array ("!3") );
+  // The '+' character in a query string may be parsed as ' '.
+  foreach (preg_split('/[+ ]/', $str_tids) as $and_tids) {
+    $tids[] = explode(',', $and_tids);
+  }
+
+  // Build title:
+  $distinct_tids = array();
+  foreach ($tids as $a) {
+    foreach ($a as $tid) {
+      $bare_tid = ltrim($tid, '!'); 
+      $distinct_tids[$bare_tid] = TRUE;
+    }
+  }
+  // This need to be fixed
+  $result = db_query('SELECT name FROM {term_data} WHERE tid IN (%s)', implode(',', array_keys($distinct_tids)));
+  $names = array();
+  while ($term = db_fetch_object($result)) {
+    $names[] = $term->name;
+  }
+  
+  // TODO: Think about removing terms to which user has no access permission
+  
+  if ($names) {
+    drupal_set_title($title = implode(', ', $names));
+
+    switch ($op) {
+      case 'page':
+        // Build breadcrumb based on first hierarchy of first term:
+        $current->tid = $tids[0][0];
+        $breadcrumbs = array(array('path' => $_GET['q']));
+        while ($parents = taxonomy_get_parents($current->tid)) {
+          $current = array_shift($parents);
+          $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name);
+        }
+        $breadcrumbs = array_reverse($breadcrumbs);
+        menu_set_location($breadcrumbs);
 
-    if ($names) {
-      drupal_set_title($title = implode(', ', $names));
+        drupal_set_html_head('<link rel="alternate" type="application/rss+xml" title="RSS - '. $title .'" href="'. url('taxonomy/term/'. $rss_tids .'/'. $depth .'/feed') .'" />');
 
-      switch ($op) {
-        case 'page':
-          // Build breadcrumb based on first hierarchy of first term:
-          $current->tid = $tids[0];
-          $breadcrumbs = array(array('path' => $_GET['q']));
-          while ($parents = taxonomy_get_parents($current->tid)) {
-            $current = array_shift($parents);
-            $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name);
-          }
-          $breadcrumbs = array_reverse($breadcrumbs);
-          menu_set_location($breadcrumbs);
-
-          drupal_set_html_head('<link rel="alternate" type="application/rss+xml" title="RSS - '. $title .'" href="'. url('taxonomy/term/'. $rss_tids .'/'. $depth .'/feed') .'" />');
-
-          $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $operator, $depth, TRUE));
-          $output .= theme('xml_icon', url("taxonomy/term/$rss_tids/$depth/feed"));
-          return $output;
-          break;
-
-        case 'feed':
-          $term = taxonomy_get_term($tids[0]);
-          $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE);
-          $channel['title'] = variable_get('site_name', 'drupal') .' - '. $title;
-          $channel['description'] = $term->description;
-
-          $result = taxonomy_select_nodes($tids, $operator, $depth, FALSE);
-          node_feed($result, $channel);
-          break;
-        default:
-          drupal_not_found();
-      }
-    }
-    else {
-      drupal_not_found();
+        $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $depth, TRUE));
+        $output .= theme('xml_icon', url("taxonomy/term/$rss_tids/$depth/feed"));
+        return $output;
+        break;
+
+      case 'feed':
+        $term = taxonomy_get_term($tids[0]);
+        $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE);
+        $channel['title'] = variable_get('site_name', 'drupal') .' - '. $title;
+        $channel['description'] = $term->description;
+
+        $result = taxonomy_select_nodes($tids, $depth, FALSE);
+        node_feed($result, $channel);
+        break;
+      default:
+        drupal_not_found();
     }
   }
+  else {
+    drupal_not_found();
+  }
 }
 
 /**
