Index: common/common.php
===================================================================
--- common/common.php	(revision 1078)
+++ common/common.php	(working copy)
@@ -246,7 +246,14 @@
     if (!array_key_exists("type", $tag)) {
         return false;
     }
-    return is_tag_name($tag["name"]) && is_tag_type($tag["type"]);
+    if (!array_key_exists("parent", $tag)) {
+        return false;
+    }
+    return (
+        is_tag_name($tag["name"]) &&
+        is_tag_type($tag["type"]) &&
+        is_tag_id($tag["parent"])
+    );
 }
 
 // Taggable objects
Index: common/db/blog.php
===================================================================
--- common/db/blog.php	(revision 1078)
+++ common/db/blog.php	(working copy)
@@ -4,7 +4,8 @@
 
 function blog_get_range($tag_name, $start, $range) {
     if (is_tag_name($tag_name)) {
-        $tag_id = tag_get_id(array("name" => $tag_name, "type" => "tag"));
+        $tag_id = tag_get_id(array(
+            "name" => $tag_name, "type" => "tag", "parent" => 0));
         if (is_null($tag_id)) {
             $tag_id = -1;
         }
@@ -37,7 +38,8 @@
 
 function blog_count($tag_name) {
     if (is_tag_name($tag_name)) {
-        $tag_id = tag_get_id(array("name" => $tag_name, "type" => "tag"));
+        $tag_id = tag_get_id(array(
+            "name" => $tag_name, "type" => "tag", "parent" => 0));
         if (is_null($tag_id)) {
             $tag_id = -1;
         }
Index: common/db/tags.php
===================================================================
--- common/db/tags.php	(revision 1078)
+++ common/db/tags.php	(working copy)
@@ -4,23 +4,31 @@
 require_once(IA_ROOT_DIR."common/common.php");
 
 // Get list of all tag names, filtered by type
-function tag_get_all($types = null) {
-    $query = "SELECT name, type FROM ia_tags";
+function tag_get_all($types = null, $parent = null) {
+    $query = "SELECT name, type, parent FROM ia_tags";
+    $where = array();
     if (!is_null($types)) {
         log_assert(is_array($types), "types should be an array");
         foreach ($types as $type) {
             log_assert(is_tag_type($type));
         }
 
-        $query .= sprintf(" WHERE type IN (%s)",
+        $where[] = sprintf("(type IN (%s))",
             implode(',', array_map('db_quote', $types))
         );
     }
+    if (!is_null($parent)) {
+        log_assert(is_tag_id($parent));
+        $where[] = sprintf("(parent = %s)", db_quote($parent));
+    }
+    if (count($where)) {
+        $query .= sprintf(" WHERE %s", implode(" AND ", $where));
+    }
     return db_fetch_all($query);
 }
 
 // Get list of all tags for a certain object, filtered by type
-function tag_get($obj, $obj_id, $type = null) {
+function tag_get($obj, $obj_id, $type = null, $parent = null) {
     log_assert(is_taggable($obj));
     log_assert(is_null($type) || is_tag_type($type));
     if (is_null($type)) {
@@ -28,13 +36,19 @@
     } else {
         $where_type = sprintf(" AND tags.type = %s", db_quote($type));
     }
+    if (is_null($parent)) {
+        $where_parent = "";
+    } else {
+        $where_parent = sprintf(" AND tags.parent = %s", db_quote($parent));
+    }
     $query = sprintf(
-        "SELECT %s_id, tag_id, tags.name AS tag_name, tags.type AS tag_type
+        "SELECT %s_id, tag_id,
+        tags.name AS tag_name, tags.type AS tag_type, tags.parent AS tag_parent
         FROM ia_%s_tags AS obj_tags
         LEFT JOIN ia_tags AS tags ON obj_tags.tag_id = tags.id
-        WHERE %s_id = %s%s",
+        WHERE %s_id = %s%s%s",
         db_escape($obj), db_escape($obj), db_escape($obj),
-        db_quote($obj_id), $where_type
+        db_quote($obj_id), $where_type, $where_parent
     );
     return db_fetch_all($query);
 }
@@ -43,8 +57,8 @@
 function tag_get_id($tag) {
     log_assert(is_tag($tag));
     $query = sprintf(
-        "SELECT id FROM ia_tags WHERE name = %s AND type = %s",
-        db_quote($tag["name"]), db_quote($tag["type"])
+        "SELECT id FROM ia_tags WHERE name = %s AND type = %s AND parent = %s",
+        db_quote($tag["name"]), db_quote($tag["type"]), db_quote($tag["parent"])
     );
     $result = db_fetch($query);
     return $result['id'];
@@ -55,8 +69,11 @@
     $tag_wheres = array();
     foreach ($tags as $tag) {
         log_assert(is_tag($tag));
-        $tag_wheres[] = sprintf("(name = %s AND type = %s)",
-            db_quote($tag["name"]), db_quote($tag["type"]));
+        $tag_wheres[] = sprintf(
+            "(name = %s AND type = %s AND parent = %s)",
+            db_quote($tag["name"]), db_quote($tag["type"]),
+            db_quote($tag["parent"])
+        );
     }
     $query = sprintf("SELECT id FROM ia_tags WHERE %s",
         implode(" OR ", $tag_wheres));
@@ -68,8 +85,11 @@
     log_assert(is_tag($tag));
     $id = tag_get_id($tag);
     if (is_null($id)) {
-        $query = sprintf("INSERT INTO ia_tags (name, type) VALUES (%s, %s)",
-            db_quote($tag['name']), db_quote($tag['type']));
+        $query = sprintf(
+            "INSERT INTO ia_tags (name, type, parent) VALUES (%s, %s, %s)",
+            db_quote($tag['name']), db_quote($tag['type']),
+            db_quote($tag['parent'])
+        );
         db_query($query);
         return db_insert_id();
     }
Index: common/tags.php
===================================================================
--- common/tags.php	(revision 1078)
+++ common/tags.php	(working copy)
@@ -30,27 +30,29 @@
     }
 }
 
-function tag_build_list($obj, $obj_id, $type, $remove_prefix = true) {
-    $tag_list = tag_get($obj, $obj_id, $type);
+function tag_build_list($obj, $obj_id, $type, $parent = null) {
+    $tag_list = tag_get($obj, $obj_id, $type, $parent);
     $tag_names = array();
     foreach ($tag_list as $tag) {
-        if ($remove_prefix) {
-            $tag_parts = explode('@', $tag['tag_name']);
-            $tag_names[] = trim($tag_parts[ count($tag_parts) - 1 ]);
-        } else {
-            $tag_names[] = $tag['tag_name'];
-        }
+        $tag_names[] = $tag['tag_name'];
     }
     return implode(", ", $tag_names);
 }
 
-function tag_update($obj, $obj_id, $type, $tag_data, $tag_prefix = "") {
+// Receives a list of tags of a certain type and, optionally, with a
+// certain parent and updates the tag list for the specified object.
+// Returns a list of tag ids.
+function tag_update($obj, $obj_id, $type, $tag_data, $parent = 0) {
     tag_clear($obj, $obj_id, $type);
     $tag_data = tag_split($tag_data);
+    $tag_ids = array();
     foreach ($tag_data as $tag_name) {
-        $tag_id = tag_assign_id(array("name" => $tag_prefix.$tag_name, "type" => $type));
+        $tag_id = tag_assign_id(
+            array("name" => $tag_name, "type" => $type, "parent" => $parent));
+        $tag_ids[] = $tag_id;
         tag_add($obj, $obj_id, $tag_id);
     }
+    return $tag_ids;
 }
 
 ?>
Index: common/textblock.php
===================================================================
--- common/textblock.php	(revision 1078)
+++ common/textblock.php	(working copy)
@@ -48,8 +48,8 @@
         // May be there is a better way to find out if $tb is tagged
         // 'stiri'. On the other hand, this operation is cached and we
         // shouldn't worry too much about performance.
-        if ($whole_news && tag_exists('textblock', $tb['name'],
-            tag_get_id(array("name" => 'stiri', "type" => "tag")))) {
+        if ($whole_news && tag_exists('textblock', $tb['name'], tag_get_id(
+            array("name" => 'stiri', "type" => "tag", "parent" => 0)))) {
             // Don't compute snippet for news -- they should be rendered as
             // they are in the snippet.
             $cache_res .= $html_text;
Index: scripts/migrate-task-tags
===================================================================
--- scripts/migrate-task-tags	(revision 1078)
+++ scripts/migrate-task-tags	(working copy)
@@ -7,8 +7,9 @@
 db_query("
     ALTER TABLE `ia_tags`
     ADD COLUMN `type` ENUM('author','contest','year','round','age_group','method','algorithm','tag') NOT NULL,
+    ADD COLUMN `parent` INTEGER NOT NULL DEFAULT 0,
     DROP INDEX `name`,
-    ADD UNIQUE INDEX `name_type` USING BTREE(`name`, `type`)
+    ADD UNIQUE INDEX `name_type` USING BTREE(`name`, `type`, `parent`)
 ");
 db_query('UPDATE `ia_tags` SET `type` = "tag"');
 
Index: www/controllers/task.php
===================================================================
--- www/controllers/task.php	(revision 1078)
+++ www/controllers/task.php	(working copy)
@@ -69,13 +69,8 @@
 
     // FIXME: tags that have children such as contest, year or round should have only one tag
     foreach ($tag_types as $type) {
-        $remove_prefix = isset($tag_parents[$type]);
-        $values['tag_'.$type] = request('tag_'.$type, tag_build_list('task', $task_id, $type, $remove_prefix));
-        $prefix['tag_'.$type] = '';
-        if (isset($tag_parents[$type])) {
-            $parent_type = $tag_parents[$type];
-            $prefix['tag_'.$type] = $prefix['tag_'.$parent_type].$values['tag_'.$parent_type]."@";
-        }
+        $values['tag_'.$type] = request('tag_'.$type,
+            tag_build_list('task', $task_id, $type));
     }
 
     // Task owner
@@ -143,6 +138,7 @@
         tag_validate($values, $errors);
 
         // If no errors then do the db monkey
+        $tags = array();
         if (!$errors) {
             // FIXME: error handling? Is that even remotely possible in php?
             task_update_parameters($task_id, $new_task_params);
@@ -150,7 +146,12 @@
 
             if (identity_can('task-tag', $new_task)) {
                 foreach ($tag_types as $type) {
-                    tag_update('task', $new_task['id'], $type, $values['tag_'.$type], $prefix['tag_'.$type]);
+                    $parent = 0;
+                    if (isset($tag_parents[$type])) {
+                        $parent = $tags[$tag_parents[$type]][0];
+                    }
+                    $tags[$type] = tag_update('task', $new_task['id'], $type,
+                        $values['tag_'.$type], $parent);
                 }
             }
 
