Managing IDs using AVL trees
Calling the number next to the ids "neighbours" is confusing, actually the ids are more like keys and the second parameter can be considered a value?

You have an AVL tree listing all the keys, and for each key, an AVL storing all the values.

You have 350000 ids with 50 values for each key. 50 is a small number, making it not completely necessary to use an AVL tree. A simple array or vector could work, it would remove a lot of the overhead of dynamically allocating nodes for each new value.

AVL trees are supposed to do every basic operation in \$O(log(n))\$, meaning insert, search, delete. Meaning, individual operations should still be very fast even after 350000 IDs are inserted (for reference, \$log_2(350000)\$ ~ 18).

The reason your program slows down should be the insert method in the AVL tree. It balances the tree after every insertion, starting from the root. Which in turn calculates the height difference and the height of both subtrees recursively:

int AVLTree::height(Node *node)
{
    if (node != NULL)
    {
        int leftHeight = height(node-&gt;left);
        int rightHeight = height(node-&gt;right);
        int maxHeight = max(leftHeight, rightHeight);
        return maxHeight + 1;
    }
    else
    {
        return 0;
    }
}


This function is actually \$O(n)\$ since it goes recursively through each child of node. Meaning, each time you get the height of a node when rebalancing the tree, it will be slow.

The solution is to keep track of the height of each node. It could look something like this for the insert function:

Node* AVLTree::insert(Node *node, int ID)
{
    if(!node) //if you want to use a comparison to null here, use nullptr instead of NULL
    {
        node = newNode(ID); //node-&gt;height should be 0 at init
        size++;
        return node;
    }

    if (node-&gt;ID == ID) {
        return node;
    }

    if(node-&gt;ID &gt; ID)
    {
        node-&gt;left = insert(node-&gt;left, ID);
    }
    else if(node-&gt;ID &lt; ID)
    {
        node-&gt;right = insert(node-&gt;right, ID);
    }

    updateHeight(node);
    balance(node);

    return node;
}


Then the updateHeight function could be like this:

void AVLTree::updateHeight(Node *node) {
    if (!node) {
        return;
    }

    node-&gt;height = max(height(node-&gt;left), height(node-&gt;right)) + 1;
}


And the height function would be simplified and non-recursive:

int AVLTree::height(Node *node)
{
    if (!node) {
        //since the value of a leaf is 0, the value of a non existing-node is -1
        //if you want 0 here the value for a new node should be 1
        return -1; 
    }
    return node-&gt;height;
}


The node's height is only updated for nodes that are traversed during the insert function, so this operation only has a complexity of \$O(log(n))\$. 

Of course you also have to update the height manually when rotating the nodes or deleting the nodes, by calling updateHeight() on the appropriate nodes. (Note that children have to have their heights updated before the parents).

There is also another area in the balance function that introduces complexity:

balance(node-&gt;left);
balance(node-&gt;right);


It will traverse the whole subtree and be applied to every node in the tree, causing a complexity of \$O(n)\$. In the insert function balance is already applied to all the ancestry of a node that gets a new child, so in the balance function you should only do recursive calls when necessary. 

I'm not sure the calls to balance the children are necessary if the rotations are properly done (maybe they are) but at least they are not necessary when the current node is already balanced, so changing the beginning of the balance function to this will reduce its complexity to \$O(log(n))\$:

void AVLTree::balance(Node *&amp;node)
{
    if (!node) 
    {
        return;
    }
    int height_diff = heightDifference(node);
    if (abs(height_diff) &lt;= 1)
    {
        return;
    }

    //...


As it will stop going down the subtree if it is balanced.

No comments on the rest of your code, it's very readable and I wouldn't sacrifice that over small optimizations when they are not needed.

PS: If you keep this:

balance(node-&gt;left);
balance(node-&gt;right);


At the end of the balance function, don't forget to call updateHeight(node) as the height of the children have potentially changed. But those two calls seem weird to me as balancing children can change their height and potentially unbalance the current node again.